Dashboard rebuilt: matches proof page design, mobile-ready
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) <noreply@anthropic.com>
This commit is contained in:
parent
5aaa3c5c08
commit
66a3460c92
@ -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; }
|
|
||||||
@ -3,30 +3,97 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Lakehouse Staffing Co-Pilot</title>
|
<title>Lakehouse — Staffing Co-Pilot</title>
|
||||||
<link rel="stylesheet" href="dashboard.css">
|
<style>
|
||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
body{font-family:'Inter','SF Pro',system-ui,sans-serif;background:#0a0a0f;color:#d4d4d8;line-height:1.6}
|
||||||
|
.hero{background:linear-gradient(135deg,#0f172a 0%,#1e1b4b 50%,#0f172a 100%);padding:24px 30px;border-bottom:1px solid #1e293b;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}
|
||||||
|
.hero h1{font-size:20px;font-weight:700;background:linear-gradient(to right,#f472b6,#818cf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
|
||||||
|
.hero .right{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
||||||
|
.dot{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:4px}
|
||||||
|
.dot.g{background:#34d399}.dot.r{background:#f87171}.dot.y{background:#fbbf24}
|
||||||
|
.svc{font-size:11px;color:#94a3b8}
|
||||||
|
.vram{font-size:11px;color:#64748b}
|
||||||
|
.btn{background:#1e293b;border:1px solid #334155;color:#e2e8f0;padding:7px 16px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:500;transition:all .15s}
|
||||||
|
.btn:hover{background:#818cf8;border-color:#818cf8}
|
||||||
|
.btn.primary{background:#7c3aed;border-color:#7c3aed}
|
||||||
|
.btn.primary:hover{background:#6d28d9}
|
||||||
|
.container{max-width:1200px;margin:0 auto;padding:20px 16px}
|
||||||
|
.grid{display:grid;grid-template-columns:5fr 3fr;gap:16px}
|
||||||
|
.card{background:#111827;border:1px solid #1e293b;border-radius:10px;padding:18px;margin-bottom:16px}
|
||||||
|
.card h2{font-size:13px;color:#818cf8;margin-bottom:12px;text-transform:uppercase;letter-spacing:1px;font-weight:600}
|
||||||
|
.tabs{display:flex;gap:6px;margin-bottom:14px;flex-wrap:wrap}
|
||||||
|
.tab{padding:5px 14px;background:#1e293b;border:1px solid #334155;color:#94a3b8;border-radius:6px;cursor:pointer;font-size:11px;font-weight:500}
|
||||||
|
.tab.active{background:#7c3aed;border-color:#7c3aed;color:#fff}
|
||||||
|
.contract{border-left:3px solid #334155;padding:12px 16px;margin-bottom:10px;background:#0d1117;border-radius:0 8px 8px 0;transition:border-color .2s}
|
||||||
|
.contract.urgent{border-left-color:#f87171}.contract.high{border-left-color:#fbbf24}.contract.filled{border-left-color:#34d399}
|
||||||
|
.contract .top{display:flex;justify-content:space-between;align-items:center}
|
||||||
|
.contract .title{font-weight:600;font-size:13px;color:#e2e8f0}
|
||||||
|
.contract .badge{font-size:10px;padding:2px 8px;border-radius:10px;font-weight:600}
|
||||||
|
.badge.urgent{background:#7f1d1d;color:#fca5a5}.badge.high{background:#78350f;color:#fcd34d}.badge.medium{background:#1e3a5f;color:#93c5fd}.badge.low{background:#14532d;color:#86efac}
|
||||||
|
.contract .meta{font-size:11px;color:#64748b;margin-top:4px}
|
||||||
|
.contract .workers{font-size:11px;color:#94a3b8;margin-top:8px}
|
||||||
|
.contract .workers b{color:#34d399}
|
||||||
|
.alert{padding:10px 14px;margin-bottom:8px;border-radius:6px;font-size:12px;display:flex;align-items:center;gap:8px}
|
||||||
|
.alert .icon{font-size:14px}
|
||||||
|
.alert.warn{background:#1c1305;border:1px solid #854d0e}.alert.info{background:#0c1a2e;border:1px solid #1e40af}.alert.good{background:#052e16;border:1px solid #166534}
|
||||||
|
.playbook{padding:10px 14px;margin-bottom:6px;background:#0d1117;border-radius:6px;font-size:11px;border:1px solid #1e293b}
|
||||||
|
.playbook .op{color:#a78bfa;font-weight:600}
|
||||||
|
.stat-row{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #1e293b;font-size:13px}
|
||||||
|
.stat-row .val{color:#34d399;font-weight:700;font-variant-numeric:tabular-nums}
|
||||||
|
.log{font-size:11px;color:#64748b;max-height:200px;overflow-y:auto}
|
||||||
|
.log div{padding:4px 0;border-bottom:1px solid #111827}
|
||||||
|
.empty{color:#475569;font-size:13px;text-align:center;padding:30px}
|
||||||
|
.proof-link{display:block;text-align:center;padding:12px;color:#818cf8;font-size:12px;text-decoration:none;border-top:1px solid #1e293b;margin-top:16px}
|
||||||
|
.proof-link:hover{color:#a78bfa}
|
||||||
|
@media(max-width:768px){
|
||||||
|
.hero{padding:16px;flex-direction:column;align-items:flex-start}
|
||||||
|
.grid{grid-template-columns:1fr}
|
||||||
|
.contract .top{flex-direction:column;align-items:flex-start;gap:4px}
|
||||||
|
.tabs{overflow-x:auto;flex-wrap:nowrap}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="header">
|
<div class="hero">
|
||||||
<h1>Lakehouse Staffing Co-Pilot</h1>
|
<h1>Staffing Co-Pilot</h1>
|
||||||
<div class="status">
|
<div class="right">
|
||||||
<span><span class="dot green" id="svc-gw"></span>Gateway</span>
|
<span class="svc"><span class="dot g" id="svc-gw"></span>Gateway</span>
|
||||||
<span><span class="dot green" id="svc-lh"></span>Lakehouse</span>
|
<span class="svc"><span class="dot g" id="svc-lh"></span>Lakehouse</span>
|
||||||
<span id="vram-display">VRAM: loading...</span>
|
<span class="vram" id="vram-display">VRAM: —</span>
|
||||||
<button class="refresh-btn" onclick="refresh()">Refresh</button>
|
<button class="btn" onclick="refresh()">Refresh</button>
|
||||||
<button class="refresh-btn" onclick="runWeek()">Run Week Sim</button>
|
<button class="btn primary" onclick="runWeek()">Run Week Sim</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid">
|
<div class="container">
|
||||||
<div>
|
<div class="grid">
|
||||||
<div class="card"><h2>Contracts</h2><div class="day-nav" id="day-nav"></div><div id="contracts">Click Run Week Sim to start</div></div>
|
<div>
|
||||||
<div class="card" style="margin-top:16px"><h2>Week Summary</h2><div id="week-stats"></div></div>
|
<div class="card">
|
||||||
</div>
|
<h2>Contracts</h2>
|
||||||
<div>
|
<div class="tabs" id="day-nav"></div>
|
||||||
<div class="card"><h2>Alerts</h2><div id="alerts"></div></div>
|
<div id="contracts"><div class="empty">Click <b>Run Week Sim</b> to simulate a staffing week</div></div>
|
||||||
<div class="card" style="margin-top:16px"><h2>Playbooks</h2><div id="playbooks"></div></div>
|
</div>
|
||||||
<div class="card" style="margin-top:16px"><h2>Live Log</h2><div class="log" id="log"></div></div>
|
<div class="card">
|
||||||
|
<h2>Week Summary</h2>
|
||||||
|
<div id="week-stats"><div class="empty">No simulation data yet</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Alerts</h2>
|
||||||
|
<div id="alerts"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Playbooks</h2>
|
||||||
|
<div id="playbooks"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Live Log</h2>
|
||||||
|
<div class="log" id="log"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<a class="proof-link" href="proof">View Proof of Work →</a>
|
||||||
</div>
|
</div>
|
||||||
<script type="module" src="dashboard.ts"></script>
|
<script type="module" src="dashboard.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
// Use the same host the browser loaded the page from — works on LAN + localhost
|
|
||||||
const GW = window.location.origin;
|
const GW = window.location.origin;
|
||||||
let simData: any = null;
|
let simData: any = null;
|
||||||
let currentDay = 0;
|
let currentDay = 0;
|
||||||
@ -11,53 +10,64 @@ async function api(path: string, body?: any) {
|
|||||||
return r.json();
|
return r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addLog(msg: string, cls = "") {
|
function addLog(msg: string) {
|
||||||
const el = document.getElementById("log")!;
|
const el = document.getElementById("log")!;
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
const t = new Date().toLocaleTimeString();
|
div.textContent = `${new Date().toLocaleTimeString()} ${msg}`;
|
||||||
div.textContent = `${t} ${msg}`;
|
|
||||||
if (cls) div.className = cls;
|
|
||||||
el.prepend(div);
|
el.prepend(div);
|
||||||
while (el.children.length > 50) el.lastChild?.remove();
|
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) {
|
function renderContracts(day: any) {
|
||||||
const el = document.getElementById("contracts")!;
|
const el = document.getElementById("contracts")!;
|
||||||
el.replaceChildren();
|
el.replaceChildren();
|
||||||
if (!day?.contracts?.length) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
for (const c of day.contracts) {
|
for (const c of day.contracts) {
|
||||||
const div = document.createElement("div");
|
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 top = document.createElement("div");
|
||||||
const title = document.createElement("div");
|
top.className = "top";
|
||||||
|
|
||||||
|
const title = document.createElement("span");
|
||||||
title.className = "title";
|
title.className = "title";
|
||||||
title.textContent = `${icon} ${c.id} - ${c.client}`;
|
title.textContent = `${c.id} — ${c.client}`;
|
||||||
div.appendChild(title);
|
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");
|
const meta = document.createElement("div");
|
||||||
meta.className = "meta";
|
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);
|
div.appendChild(meta);
|
||||||
|
|
||||||
if (c.matches?.length) {
|
if (c.matches?.length) {
|
||||||
const workers = document.createElement("div");
|
const workers = document.createElement("div");
|
||||||
workers.className = "workers";
|
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);
|
div.appendChild(workers);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.notes) {
|
if (c.notes) {
|
||||||
const notes = document.createElement("div");
|
const notes = document.createElement("div");
|
||||||
notes.className = "meta";
|
notes.className = "meta";
|
||||||
|
notes.style.marginTop = "4px";
|
||||||
|
notes.style.fontStyle = "italic";
|
||||||
notes.textContent = c.notes;
|
notes.textContent = c.notes;
|
||||||
div.appendChild(notes);
|
div.appendChild(notes);
|
||||||
}
|
}
|
||||||
@ -72,8 +82,10 @@ function renderDayNav() {
|
|||||||
if (!simData?.days) return;
|
if (!simData?.days) return;
|
||||||
simData.days.forEach((d: any, i: number) => {
|
simData.days.forEach((d: any, i: number) => {
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.className = `day-btn ${i === currentDay ? "active" : ""}`;
|
btn.className = `tab ${i === currentDay ? "active" : ""}`;
|
||||||
btn.textContent = d.label;
|
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]); };
|
btn.onclick = () => { currentDay = i; renderDayNav(); renderContracts(simData.days[i]); };
|
||||||
nav.appendChild(btn);
|
nav.appendChild(btn);
|
||||||
});
|
});
|
||||||
@ -84,25 +96,25 @@ function renderWeekStats() {
|
|||||||
el.replaceChildren();
|
el.replaceChildren();
|
||||||
if (!simData?.summary) return;
|
if (!simData?.summary) return;
|
||||||
const s = simData.summary;
|
const s = simData.summary;
|
||||||
const rows = [
|
const rows: [string, string][] = [
|
||||||
["Total contracts", s.total_contracts],
|
["Total contracts", String(s.total_contracts)],
|
||||||
["Total positions", s.total_needed],
|
["Total positions", String(s.total_needed)],
|
||||||
["Filled", s.total_filled],
|
["Filled", String(s.total_filled)],
|
||||||
["Fill rate", `${s.fill_pct}%`],
|
["Fill rate", `${s.fill_pct}%`],
|
||||||
["Emergencies", s.emergencies],
|
["Emergencies", String(s.emergencies)],
|
||||||
["Handoffs", s.handoffs],
|
["Handoffs", String(s.handoffs)],
|
||||||
["Playbooks", s.playbook_entries],
|
["Playbooks", String(s.playbook_entries)],
|
||||||
];
|
];
|
||||||
for (const [label, val] of rows) {
|
for (const [label, val] of rows) {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "stat-row";
|
row.className = "stat-row";
|
||||||
const nameSpan = document.createElement("span");
|
const l = document.createElement("span");
|
||||||
nameSpan.textContent = String(label);
|
l.textContent = label;
|
||||||
const valSpan = document.createElement("span");
|
const v = document.createElement("span");
|
||||||
valSpan.className = "val";
|
v.className = "val";
|
||||||
valSpan.textContent = String(val);
|
v.textContent = val;
|
||||||
row.appendChild(nameSpan);
|
row.appendChild(l);
|
||||||
row.appendChild(valSpan);
|
row.appendChild(v);
|
||||||
el.appendChild(row);
|
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" });
|
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 || []) {
|
for (const row of r.rows || []) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "alert warn";
|
div.className = `alert ${row.archetype === "erratic" ? "warn" : "info"}`;
|
||||||
div.textContent = `${row.archetype}: ${row.cnt} workers flagged`;
|
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);
|
el.appendChild(div);
|
||||||
}
|
}
|
||||||
const info = document.createElement("div");
|
const good = document.createElement("div");
|
||||||
info.className = "alert info";
|
good.className = "alert good";
|
||||||
info.textContent = "System: all services running";
|
const gi = document.createElement("span");
|
||||||
el.appendChild(info);
|
gi.className = "icon";
|
||||||
|
gi.textContent = "✓";
|
||||||
|
good.appendChild(gi);
|
||||||
|
good.appendChild(document.createTextNode("All services running"));
|
||||||
|
el.appendChild(good);
|
||||||
} catch {
|
} catch {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "alert warn";
|
div.className = "alert warn";
|
||||||
@ -136,11 +156,14 @@ async function loadPlaybooks() {
|
|||||||
try {
|
try {
|
||||||
const r = await api("/playbooks", {});
|
const r = await api("/playbooks", {});
|
||||||
const pbs = r.playbooks || [];
|
const pbs = r.playbooks || [];
|
||||||
if (pbs.length === 0) {
|
if (!pbs.length) {
|
||||||
el.textContent = "No playbooks yet - run the simulation";
|
const empty = document.createElement("div");
|
||||||
|
empty.className = "empty";
|
||||||
|
empty.textContent = "Run the simulation to build playbooks";
|
||||||
|
el.appendChild(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const p of pbs.slice(0, 8)) {
|
for (const p of pbs.slice(0, 6)) {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "playbook";
|
div.className = "playbook";
|
||||||
const op = document.createElement("span");
|
const op = document.createElement("span");
|
||||||
@ -152,39 +175,43 @@ async function loadPlaybooks() {
|
|||||||
el.appendChild(div);
|
el.appendChild(div);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
el.textContent = "Could not load playbooks";
|
const el2 = document.getElementById("playbooks")!;
|
||||||
|
el2.textContent = "Could not load playbooks";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkServices() {
|
async function checkServices() {
|
||||||
try {
|
try {
|
||||||
await fetch(GW + "/health");
|
await fetch(GW + "/health");
|
||||||
document.getElementById("svc-gw")!.className = "dot green";
|
document.getElementById("svc-gw")!.className = "dot g";
|
||||||
} catch {
|
} catch {
|
||||||
document.getElementById("svc-gw")!.className = "dot red";
|
document.getElementById("svc-gw")!.className = "dot r";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const r = await api("/vram");
|
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 {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Week simulation runner
|
|
||||||
(window as any).runWeek = async function () {
|
(window as any).runWeek = async function () {
|
||||||
addLog("Starting week simulation...");
|
addLog("Starting week simulation...");
|
||||||
try {
|
try {
|
||||||
const r = await fetch(GW + "/simulation/run", { method: "POST" });
|
const r = await fetch(GW + "/simulation/run", { method: "POST" });
|
||||||
simData = await r.json();
|
simData = await r.json();
|
||||||
if (simData.error) {
|
if (simData.error) {
|
||||||
addLog(`Error: ${simData.error}`, "fail");
|
addLog(`Error: ${simData.error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
renderDayNav();
|
renderDayNav();
|
||||||
|
currentDay = 0;
|
||||||
renderContracts(simData.days[0]);
|
renderContracts(simData.days[0]);
|
||||||
renderWeekStats();
|
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) {
|
} catch (e) {
|
||||||
addLog(`Simulation failed: ${e}`, "fail");
|
addLog(`Failed: ${e}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -195,8 +222,9 @@ async function checkServices() {
|
|||||||
await loadPlaybooks();
|
await loadPlaybooks();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial load
|
// Init
|
||||||
checkServices();
|
checkServices();
|
||||||
loadAlerts();
|
loadAlerts();
|
||||||
loadPlaybooks();
|
loadPlaybooks();
|
||||||
|
addLog("Dashboard loaded");
|
||||||
setInterval(checkServices, 30000);
|
setInterval(checkServices, 30000);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user