adding a wizard for setup

This commit is contained in:
Ace
2026-06-26 12:08:32 +02:00
parent a4d2ad143b
commit 5feb95187c
3 changed files with 605 additions and 54 deletions
+139
View File
@@ -1,6 +1,7 @@
import http from "node:http"
import fs from "node:fs"
import path from "node:path"
import { spawn } from "node:child_process"
import { fileURLToPath } from "node:url"
const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -34,6 +35,10 @@ const DATABASE_URL = process.env.DATABASE_URL
const JOBS_PATH = process.env.JOBS_PATH || path.join(ROOT, "data", "ai", "jobs.jsonl")
const AI_MD_PATH = process.env.AI_MD_PATH || path.join(ROOT, "data", "ai", "ai.md")
// ── Setup state ──────────────────────────────────────────────────
let pullProcess = null
let pullProgress = { status: "idle", progress: 0, message: "" }
// ── Job loading ─────────────────────────────────────────────────
function loadJobs() {
try {
@@ -297,6 +302,140 @@ const server = http.createServer(async (req, res) => {
return sendJSON(res, 200, results)
}
// ── Setup endpoints ─────────────────────────────────────────
// GET /setup/status — check environment
if (req.method === "GET" && pathname === "/setup/status") {
const envExists = fs.existsSync(path.join(ROOT, ".env.local"))
// Ollama check
let ollamaRunning = false
try {
await fetch(`${OLLAMA_URL}/api/tags`, { signal: AbortSignal.timeout(3000) })
ollamaRunning = true
} catch {}
// Model check
let modelAvailable = false
if (ollamaRunning) {
try {
const r = await fetch(`${OLLAMA_URL}/api/show`, {
method: "POST", body: JSON.stringify({ name: MODEL }),
signal: AbortSignal.timeout(5000),
})
modelAvailable = r.ok
} catch {}
}
// Profile auto-detect
let profilePath = process.env.FX_PROFILE || ""
let profileDetected = !!profilePath
if (!profileDetected) {
try {
const r = await fetch("http://127.0.0.1:3008/health", { signal: AbortSignal.timeout(2000) })
if (r.ok) {
const diag = await (await fetch("http://127.0.0.1:3008/setup/profile", { signal: AbortSignal.timeout(5000) })).json()
if (diag.path) { profilePath = diag.path; profileDetected = true }
}
} catch {}
}
// Login check
let facebookLoggedIn = false
if (profileDetected) {
try {
const r = await fetch("http://127.0.0.1:3008/setup/check-login", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ profile_path: profilePath }),
signal: AbortSignal.timeout(15000),
})
if (r.ok) { const d = await r.json(); facebookLoggedIn = d.logged_in === true }
} catch {}
}
const firstRun = !envExists || !ollamaRunning || !profileDetected || !modelAvailable
return sendJSON(res, 200, {
first_run: firstRun,
env_exists: envExists,
ollama_running: ollamaRunning,
model_available: modelAvailable,
model_name: MODEL,
profile_detected: profileDetected,
profile_path: profilePath || null,
facebook_logged_in: facebookLoggedIn,
})
}
// POST /setup/profile — save profile path to .env.local
if (req.method === "POST" && pathname === "/setup/profile") {
const body = await parseBody(req)
const profilePath = (body.path || "").trim()
if (!profilePath) return sendJSON(res, 400, { error: "Path required" })
if (!fs.existsSync(profilePath)) return sendJSON(res, 400, { error: "Path does not exist" })
const envPath = path.join(ROOT, ".env.local")
let content = ""
try { content = fs.readFileSync(envPath, "utf-8") } catch {}
const lines = content.split("\n")
let found = false
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim().startsWith("FX_PROFILE=")) {
lines[i] = `FX_PROFILE=${profilePath}`
found = true
break
}
}
if (!found) lines.push(`FX_PROFILE=${profilePath}`)
fs.writeFileSync(envPath, lines.join("\n"), "utf-8")
process.env.FX_PROFILE = profilePath
return sendJSON(res, 200, { success: true, path: profilePath })
}
// POST /setup/check-login — verify Facebook login in the given profile
if (req.method === "POST" && pathname === "/setup/check-login") {
const body = await parseBody(req)
const profilePath = body.profile_path || process.env.FX_PROFILE || ""
if (!profilePath) return sendJSON(res, 200, { logged_in: false, reason: "no_profile" })
try {
const r = await fetch("http://127.0.0.1:3008/setup/check-login", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ profile_path: profilePath }),
signal: AbortSignal.timeout(20000),
})
if (r.ok) { const d = await r.json(); return sendJSON(res, 200, d) }
} catch {}
return sendJSON(res, 200, { logged_in: false, reason: "scraper_unavailable" })
}
// POST /setup/ollama/pull — start pulling the model
if (req.method === "POST" && pathname === "/setup/ollama/pull") {
if (pullProcess) return sendJSON(res, 200, { status: "already_running" })
pullProgress = { status: "downloading", progress: 0, message: "Starting..." }
const isWin = process.platform === "win32"
const cmd = isWin ? "ollama.exe" : "ollama"
pullProcess = spawn(cmd, ["pull", MODEL], { stdio: ["ignore", "pipe", "pipe"] })
pullProcess.stdout.on("data", (data) => {
const text = data.toString()
pullProgress.message = text.trim()
// Extract percentage from patterns like "pulling xxxx... 45%"
const m = text.match(/(\d+)%/)
if (m) pullProgress.progress = parseInt(m[1], 10)
})
pullProcess.on("close", (code) => {
pullProcess = null
pullProgress.status = code === 0 ? "done" : "failed"
if (code === 0) pullProgress.progress = 100
})
return sendJSON(res, 200, { status: "started" })
}
// GET /setup/ollama/pull/progress
if (req.method === "GET" && pathname === "/setup/ollama/pull/progress") {
return sendJSON(res, 200, pullProgress)
}
// GET /ai/jobs
if (req.method === "GET" && pathname === "/ai/jobs") {
return sendJSON(res, 200, { jobs: loadedJobs })