lakehouse/mcp-server/dashboard.ts
root 48c7c1c5e6 Fix dashboard: detect /lakehouse/ nginx prefix for API calls
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>
2026-04-17 13:04:24 -05:00

233 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);