// Detect if we're behind nginx (/lakehouse/ prefix) or direct (:3700) const base = window.location.pathname.startsWith("/lakehouse") ? "/lakehouse" : ""; const GW = window.location.origin + base; let simData: any = null; let currentDay = 0; async function api(path: string, body?: any) { const opts: RequestInit = body ? { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) } : {}; const r = await fetch(GW + path, opts); return r.json(); } function addLog(msg: string) { const el = document.getElementById("log")!; const div = document.createElement("div"); div.textContent = `${new Date().toLocaleTimeString()} ${msg}`; el.prepend(div); while (el.children.length > 50) el.lastChild?.remove(); } function renderContracts(day: any) { const el = document.getElementById("contracts")!; el.replaceChildren(); if (!day?.contracts?.length) { const empty = document.createElement("div"); empty.className = "empty"; empty.textContent = "No contracts for this day"; el.appendChild(empty); return; } for (const c of day.contracts) { const div = document.createElement("div"); const isFilled = c.filled >= c.headcount; div.className = `contract ${isFilled ? "filled" : c.priority === "urgent" ? "urgent" : c.priority === "high" ? "high" : ""}`; const top = document.createElement("div"); top.className = "top"; const title = document.createElement("span"); title.className = "title"; title.textContent = `${c.id} — ${c.client}`; top.appendChild(title); const badge = document.createElement("span"); badge.className = `badge ${c.priority}`; badge.textContent = c.priority.toUpperCase(); top.appendChild(badge); div.appendChild(top); const meta = document.createElement("div"); meta.className = "meta"; meta.textContent = `${c.role} × ${c.headcount} · ${c.city || c.state} · Start: ${c.start} · ${c.filled}/${c.headcount} filled`; div.appendChild(meta); if (c.matches?.length) { const workers = document.createElement("div"); workers.className = "workers"; const names = c.matches.map((m: any) => m.name || m.doc_id).join(", "); const b = document.createElement("b"); b.textContent = `${c.matches.length} matched: `; workers.appendChild(b); workers.appendChild(document.createTextNode(names)); div.appendChild(workers); } if (c.notes) { const notes = document.createElement("div"); notes.className = "meta"; notes.style.marginTop = "4px"; notes.style.fontStyle = "italic"; notes.textContent = c.notes; div.appendChild(notes); } el.appendChild(div); } } function renderDayNav() { const nav = document.getElementById("day-nav")!; nav.replaceChildren(); if (!simData?.days) return; simData.days.forEach((d: any, i: number) => { const btn = document.createElement("button"); btn.className = `tab ${i === currentDay ? "active" : ""}`; const filled = d.contracts.reduce((s: number, c: any) => s + c.filled, 0); const needed = d.contracts.reduce((s: number, c: any) => s + c.headcount, 0); btn.textContent = `${d.label} (${filled}/${needed})`; btn.onclick = () => { currentDay = i; renderDayNav(); renderContracts(simData.days[i]); }; nav.appendChild(btn); }); } function renderWeekStats() { const el = document.getElementById("week-stats")!; el.replaceChildren(); if (!simData?.summary) return; const s = simData.summary; const rows: [string, string][] = [ ["Total contracts", String(s.total_contracts)], ["Total positions", String(s.total_needed)], ["Filled", String(s.total_filled)], ["Fill rate", `${s.fill_pct}%`], ["Emergencies", String(s.emergencies)], ["Handoffs", String(s.handoffs)], ["Playbooks", String(s.playbook_entries)], ]; for (const [label, val] of rows) { const row = document.createElement("div"); row.className = "stat-row"; const l = document.createElement("span"); l.textContent = label; const v = document.createElement("span"); v.className = "val"; v.textContent = val; row.appendChild(l); row.appendChild(v); el.appendChild(row); } } async function loadAlerts() { const el = document.getElementById("alerts")!; el.replaceChildren(); try { const r = await api("/sql", { sql: "SELECT archetype, COUNT(*) cnt FROM ethereal_workers WHERE archetype IN ('erratic','silent') GROUP BY archetype" }); for (const row of r.rows || []) { const div = document.createElement("div"); div.className = `alert ${row.archetype === "erratic" ? "warn" : "info"}`; const icon = document.createElement("span"); icon.className = "icon"; icon.textContent = row.archetype === "erratic" ? "⚠" : "📵"; div.appendChild(icon); div.appendChild(document.createTextNode(`${row.cnt} ${row.archetype} workers flagged`)); el.appendChild(div); } const good = document.createElement("div"); good.className = "alert good"; const gi = document.createElement("span"); gi.className = "icon"; gi.textContent = "✓"; good.appendChild(gi); good.appendChild(document.createTextNode("All services running")); el.appendChild(good); } catch { const div = document.createElement("div"); div.className = "alert warn"; div.textContent = "Could not load alerts"; el.appendChild(div); } } async function loadPlaybooks() { const el = document.getElementById("playbooks")!; el.replaceChildren(); try { const r = await api("/playbooks", {}); const pbs = r.playbooks || []; if (!pbs.length) { const empty = document.createElement("div"); empty.className = "empty"; empty.textContent = "Run the simulation to build playbooks"; el.appendChild(empty); return; } for (const p of pbs.slice(0, 6)) { const div = document.createElement("div"); div.className = "playbook"; const op = document.createElement("span"); op.className = "op"; op.textContent = (p.operation || "").slice(0, 50); div.appendChild(op); div.appendChild(document.createElement("br")); div.appendChild(document.createTextNode((p.result || "").slice(0, 80))); el.appendChild(div); } } catch { const el2 = document.getElementById("playbooks")!; el2.textContent = "Could not load playbooks"; } } async function checkServices() { try { await fetch(GW + "/health"); document.getElementById("svc-gw")!.className = "dot g"; } catch { document.getElementById("svc-gw")!.className = "dot r"; } try { const r = await api("/vram"); const used = r.gpu?.used_mib || "?"; const total = r.gpu?.total_mib || "?"; const models = (r.ollama_loaded || []).map((m: any) => m.name).join(", "); document.getElementById("vram-display")!.textContent = `GPU: ${used}/${total} MiB ${models ? "· " + models : ""}`; } catch {} } (window as any).runWeek = async function () { addLog("Starting week simulation..."); try { const r = await fetch(GW + "/simulation/run", { method: "POST" }); simData = await r.json(); if (simData.error) { addLog(`Error: ${simData.error}`); return; } renderDayNav(); currentDay = 0; renderContracts(simData.days[0]); renderWeekStats(); addLog(`Week done: ${simData.summary.total_filled}/${simData.summary.total_needed} filled (${simData.summary.fill_pct}%)`); } catch (e) { addLog(`Failed: ${e}`); } }; (window as any).refresh = async function () { addLog("Refreshing..."); await checkServices(); await loadAlerts(); await loadPlaybooks(); }; // Init checkServices(); loadAlerts(); loadPlaybooks(); addLog("Dashboard loaded"); setInterval(checkServices, 30000);