From 66a3460c9268211e80723881f64d12a8711b54d0 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 17 Apr 2026 12:51:08 -0500 Subject: [PATCH] Dashboard rebuilt: matches proof page design, mobile-ready MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clean dark theme matching /proof page. Priority badges on contracts (urgent=red, high=yellow, medium=blue, low=green). Worker matches shown inline. Day tabs show fill counts. Alerts with icons. Playbook entries styled. All styles inline — no separate CSS file. Mobile responsive: single column layout, scrollable tabs. Links to /proof at bottom. https://devop.live/lakehouse/ — the dashboard https://devop.live/lakehouse/proof — the proof page Co-Authored-By: Claude Opus 4.6 (1M context) --- mcp-server/dashboard.css | 22 ------- mcp-server/dashboard.html | 105 ++++++++++++++++++++++++------ mcp-server/dashboard.ts | 132 +++++++++++++++++++++++--------------- 3 files changed, 166 insertions(+), 93 deletions(-) delete mode 100644 mcp-server/dashboard.css diff --git a/mcp-server/dashboard.css b/mcp-server/dashboard.css deleted file mode 100644 index 0842307..0000000 --- a/mcp-server/dashboard.css +++ /dev/null @@ -1,22 +0,0 @@ -* { margin: 0; padding: 0; box-sizing: border-box; } -body { font-family: 'SF Mono', 'Fira Code', monospace; background: #0a0a0f; color: #e0e0e0; } -.header { background: linear-gradient(135deg, #1a1a2e, #16213e); padding: 20px 30px; border-bottom: 2px solid #0f3460; display: flex; justify-content: space-between; align-items: center; } -.header h1 { font-size: 20px; color: #e94560; } -.header .status { display: flex; gap: 15px; font-size: 12px; align-items: center; } -.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 4px; } -.dot.green { background: #00ff88; } .dot.red { background: #ff4444; } -.grid { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; padding: 16px; max-width: 1400px; margin: 0 auto; } -.card { background: #12121a; border: 1px solid #1a1a2e; border-radius: 8px; padding: 16px; } -.card h2 { font-size: 14px; color: #e94560; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 1px; } -.contract { border-left: 3px solid #0f3460; padding: 10px 14px; margin-bottom: 10px; background: #0d0d15; border-radius: 0 6px 6px 0; } -.contract.urgent { border-left-color: #e94560; } .contract.high { border-left-color: #ff8c00; } .contract.filled { border-left-color: #00ff88; } -.contract .title { font-weight: bold; font-size: 13px; } .contract .meta { font-size: 11px; color: #888; margin-top: 4px; } -.contract .workers { font-size: 11px; color: #aaa; margin-top: 6px; } .contract .workers b { color: #00ff88; } -.alert { padding: 8px 12px; margin-bottom: 8px; border-radius: 4px; font-size: 12px; } -.alert.warn { background: #2a1a00; border-left: 3px solid #ffaa00; } .alert.info { background: #001a2a; border-left: 3px solid #00aaff; } .alert.good { background: #002a1a; border-left: 3px solid #00ff88; } -.playbook { padding: 8px 12px; margin-bottom: 6px; background: #0d0d15; border-radius: 4px; font-size: 11px; } .playbook .op { color: #e94560; } -.stat-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #1a1a2e; font-size: 13px; } .stat-row .val { color: #00ff88; font-weight: bold; } -.day-nav { display: flex; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; } -.day-btn { padding: 6px 14px; background: #1a1a2e; border: 1px solid #0f3460; color: #e0e0e0; border-radius: 4px; cursor: pointer; font-size: 12px; } .day-btn.active { background: #e94560; border-color: #e94560; color: #fff; } -.log { font-size: 11px; color: #888; max-height: 250px; overflow-y: auto; } .log div { padding: 3px 0; border-bottom: 1px solid #0d0d15; } -.refresh-btn { background: #0f3460; border: none; color: #e0e0e0; padding: 6px 16px; border-radius: 4px; cursor: pointer; font-size: 12px; } .refresh-btn:hover { background: #e94560; } diff --git a/mcp-server/dashboard.html b/mcp-server/dashboard.html index 712930d..f87df4b 100644 --- a/mcp-server/dashboard.html +++ b/mcp-server/dashboard.html @@ -3,30 +3,97 @@ -Lakehouse Staffing Co-Pilot - +Lakehouse — Staffing Co-Pilot + -
-

Lakehouse Staffing Co-Pilot

-
- Gateway - Lakehouse - VRAM: loading... - - +
+

Staffing Co-Pilot

+
+ Gateway + Lakehouse + VRAM: — + +
-
-
-

Contracts

Click Run Week Sim to start
-

Week Summary

-
-
-

Alerts

-

Playbooks

-

Live Log

+
+
+
+
+

Contracts

+
+
Click Run Week Sim to simulate a staffing week
+
+
+

Week Summary

+
No simulation data yet
+
+
+
+
+

Alerts

+
+
+
+

Playbooks

+
+
+
+

Live Log

+
+
+
+ View Proof of Work →
diff --git a/mcp-server/dashboard.ts b/mcp-server/dashboard.ts index 2cc106c..8947267 100644 --- a/mcp-server/dashboard.ts +++ b/mcp-server/dashboard.ts @@ -1,4 +1,3 @@ -// Use the same host the browser loaded the page from — works on LAN + localhost const GW = window.location.origin; let simData: any = null; let currentDay = 0; @@ -11,53 +10,64 @@ async function api(path: string, body?: any) { return r.json(); } -function addLog(msg: string, cls = "") { +function addLog(msg: string) { const el = document.getElementById("log")!; const div = document.createElement("div"); - const t = new Date().toLocaleTimeString(); - div.textContent = `${t} ${msg}`; - if (cls) div.className = cls; + div.textContent = `${new Date().toLocaleTimeString()} ${msg}`; el.prepend(div); while (el.children.length > 50) el.lastChild?.remove(); } -function setText(id: string, text: string) { - const el = document.getElementById(id); - if (el) el.textContent = text; -} - function renderContracts(day: any) { const el = document.getElementById("contracts")!; el.replaceChildren(); if (!day?.contracts?.length) { - el.textContent = "No contracts for this day"; + 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"); - div.className = `contract ${c.filled >= c.headcount ? "filled" : c.priority === "urgent" ? "urgent" : c.priority === "high" ? "high" : ""}`; + const isFilled = c.filled >= c.headcount; + div.className = `contract ${isFilled ? "filled" : c.priority === "urgent" ? "urgent" : c.priority === "high" ? "high" : ""}`; - const icon = c.priority === "urgent" ? "!!" : c.priority === "high" ? "!" : ""; - const title = document.createElement("div"); + const top = document.createElement("div"); + top.className = "top"; + + const title = document.createElement("span"); title.className = "title"; - title.textContent = `${icon} ${c.id} - ${c.client}`; - div.appendChild(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} x${c.headcount} | ${c.city || c.state} | ${c.filled}/${c.headcount} filled | ${c.priority}`; + 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"; - workers.textContent = c.matches.map((m: any) => `${m.name || m.doc_id} (${m.score?.toFixed(2) || "?"})`).join(", "); + 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); } @@ -72,8 +82,10 @@ function renderDayNav() { if (!simData?.days) return; simData.days.forEach((d: any, i: number) => { const btn = document.createElement("button"); - btn.className = `day-btn ${i === currentDay ? "active" : ""}`; - btn.textContent = d.label; + 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); }); @@ -84,25 +96,25 @@ function renderWeekStats() { el.replaceChildren(); if (!simData?.summary) return; const s = simData.summary; - const rows = [ - ["Total contracts", s.total_contracts], - ["Total positions", s.total_needed], - ["Filled", s.total_filled], + 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", s.emergencies], - ["Handoffs", s.handoffs], - ["Playbooks", s.playbook_entries], + ["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 nameSpan = document.createElement("span"); - nameSpan.textContent = String(label); - const valSpan = document.createElement("span"); - valSpan.className = "val"; - valSpan.textContent = String(val); - row.appendChild(nameSpan); - row.appendChild(valSpan); + 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); } } @@ -114,14 +126,22 @@ async function loadAlerts() { 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 warn"; - div.textContent = `${row.archetype}: ${row.cnt} workers flagged`; + 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 info = document.createElement("div"); - info.className = "alert info"; - info.textContent = "System: all services running"; - el.appendChild(info); + 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"; @@ -136,11 +156,14 @@ async function loadPlaybooks() { try { const r = await api("/playbooks", {}); const pbs = r.playbooks || []; - if (pbs.length === 0) { - el.textContent = "No playbooks yet - run the simulation"; + 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, 8)) { + for (const p of pbs.slice(0, 6)) { const div = document.createElement("div"); div.className = "playbook"; const op = document.createElement("span"); @@ -152,39 +175,43 @@ async function loadPlaybooks() { el.appendChild(div); } } catch { - el.textContent = "Could not load playbooks"; + 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 green"; + document.getElementById("svc-gw")!.className = "dot g"; } catch { - document.getElementById("svc-gw")!.className = "dot red"; + document.getElementById("svc-gw")!.className = "dot r"; } try { const r = await api("/vram"); - setText("vram-display", `VRAM: ${r.gpu?.used_mib || "?"}/${r.gpu?.total_mib || "?"} MiB`); + 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 {} } -// Week simulation runner (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}`, "fail"); + addLog(`Error: ${simData.error}`); return; } renderDayNav(); + currentDay = 0; renderContracts(simData.days[0]); renderWeekStats(); - addLog(`Week complete: ${simData.summary?.total_filled}/${simData.summary?.total_needed} filled`, "ok"); + addLog(`Week done: ${simData.summary.total_filled}/${simData.summary.total_needed} filled (${simData.summary.fill_pct}%)`); } catch (e) { - addLog(`Simulation failed: ${e}`, "fail"); + addLog(`Failed: ${e}`); } }; @@ -195,8 +222,9 @@ async function checkServices() { await loadPlaybooks(); }; -// Initial load +// Init checkServices(); loadAlerts(); loadPlaybooks(); +addLog("Dashboard loaded"); setInterval(checkServices, 30000);