dashboard.ts now checks if running behind the nginx proxy (path starts with /lakehouse) and prepends the prefix to all API calls. Without this, the browser called /sql instead of /lakehouse/sql and got 404s from the LLM Team Flask app. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
233 lines
7.7 KiB
TypeScript
233 lines
7.7 KiB
TypeScript
// 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);
|