Search UI: type what you need, see real workers — no more taking my word for it

Rebuilt the dashboard into a live search interface anyone can use:
- Big search box: type in plain English, hit Enter or click Search
- 3 modes: AI Search, CRM Keyword, Hybrid (best)
- Clickable examples: 'warehouse help', 'dependable machine operator', etc
- Filters: state, role, min reliability
- Results show: name, role, location, skills, certs, reliability, AI match score
- Hybrid results marked 'SQL verified against database'
- CRM mode shows 0 results with a prompt to try AI Search
- Mobile responsive

This is the answer to 'we just have to take your word for it.'
Type anything. See real workers. Compare CRM vs AI side by side.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
root 2026-04-17 13:06:31 -05:00
parent 48c7c1c5e6
commit 6a2cc0fb8f
2 changed files with 327 additions and 310 deletions

View File

@ -3,98 +3,346 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Lakehouse — Staffing Co-Pilot</title>
<title>Lakehouse — Staffing Search</title>
<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}
.hero{background:linear-gradient(135deg,#0f172a 0%,#1e1b4b 50%,#0f172a 100%);padding:30px;border-bottom:1px solid #1e293b;text-align:center}
.hero h1{font-size:24px;font-weight:700;background:linear-gradient(to right,#f472b6,#818cf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:6px}
.hero p{color:#94a3b8;font-size:13px}
.container{max-width:900px;margin:0 auto;padding:24px 16px}
.search-box{background:#111827;border:2px solid #334155;border-radius:12px;padding:20px;margin-bottom:24px;transition:border-color .2s}
.search-box:focus-within{border-color:#818cf8}
.search-row{display:flex;gap:10px}
.search-input{flex:1;background:#0a0a0f;border:1px solid #1e293b;border-radius:8px;padding:14px 18px;color:#e2e8f0;font-size:15px;outline:none}
.search-input::placeholder{color:#475569}
.search-btn{background:#7c3aed;border:none;color:#fff;padding:14px 28px;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;white-space:nowrap}
.search-btn:hover{background:#6d28d9}
.search-btn:disabled{background:#334155;cursor:wait}
.filters{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
.filter{background:#1e293b;border:1px solid #334155;border-radius:6px;padding:6px 12px;font-size:12px;color:#94a3b8;outline:none}
.filter select,.filter input{background:transparent;border:none;color:#e2e8f0;font-size:12px;outline:none;width:auto}
.filter select{cursor:pointer}
.filter label{color:#64748b;margin-right:4px}
.examples{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
.example{background:#0d1117;border:1px solid #1e293b;border-radius:20px;padding:4px 14px;font-size:11px;color:#818cf8;cursor:pointer;transition:all .15s}
.example:hover{background:#1e1b4b;border-color:#818cf8}
.results{margin-top:8px}
.result-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;flex-wrap:wrap;gap:8px}
.result-header .count{font-size:14px;color:#e2e8f0;font-weight:600}
.result-header .meta{font-size:11px;color:#64748b}
.badge{display:inline-block;padding:2px 10px;border-radius:20px;font-size:10px;font-weight:600}
.badge.green{background:#052e16;color:#34d399;border:1px solid #166534}
.badge.purple{background:#1e1047;color:#a78bfa;border:1px solid #5b21b6}
.badge.blue{background:#0c1a3d;color:#60a5fa;border:1px solid #1e40af}
.worker{background:#111827;border:1px solid #1e293b;border-radius:10px;padding:18px;margin-bottom:12px;transition:border-color .2s}
.worker:hover{border-color:#334155}
.worker .top{display:flex;justify-content:space-between;align-items:flex-start;gap:12px}
.worker .name{font-size:16px;font-weight:700;color:#e2e8f0}
.worker .role{font-size:13px;color:#818cf8;margin-top:2px}
.worker .location{font-size:12px;color:#64748b;margin-top:2px}
.worker .score{text-align:right}
.worker .score .num{font-size:24px;font-weight:800;color:#34d399}
.worker .score .label{font-size:10px;color:#64748b;text-transform:uppercase}
.worker .details{display:flex;gap:16px;margin-top:12px;flex-wrap:wrap}
.worker .detail{font-size:11px}
.worker .detail .dt{color:#64748b;text-transform:uppercase;font-size:10px;letter-spacing:0.5px}
.worker .detail .dd{color:#d4d4d8;margin-top:2px}
.worker .verified{display:inline-flex;align-items:center;gap:4px;font-size:10px;color:#34d399;margin-top:8px}
.empty{text-align:center;color:#475569;padding:40px;font-size:14px}
.loading{text-align:center;padding:30px;color:#818cf8}
.mode-toggle{display:flex;gap:4px;background:#0d1117;border-radius:8px;padding:3px;margin-bottom:16px}
.mode-btn{padding:8px 16px;border-radius:6px;border:none;cursor:pointer;font-size:12px;font-weight:500;color:#94a3b8;background:transparent}
.mode-btn.active{background:#7c3aed;color:#fff}
.footer{text-align:center;padding:20px;color:#475569;font-size:11px;border-top:1px solid #1e293b;margin-top:30px}
.footer a{color:#818cf8;text-decoration:none}
@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}
.search-row{flex-direction:column}
.worker .top{flex-direction:column}
.worker .score{text-align:left}
.worker .details{flex-direction:column;gap:8px}
.filters{flex-direction:column}
}
</style>
</head>
<body>
<div class="hero">
<h1>Staffing Co-Pilot</h1>
<div class="right">
<span class="svc"><span class="dot g" id="svc-gw"></span>Gateway</span>
<span class="svc"><span class="dot g" id="svc-lh"></span>Lakehouse</span>
<span class="vram" id="vram-display">VRAM: —</span>
<button class="btn" onclick="refresh()">Refresh</button>
<button class="btn primary" onclick="runWeek()">Run Week Sim</button>
</div>
<h1>Search 500,000 Workers</h1>
<p>Type what you need in plain English — the AI understands meaning, not just keywords</p>
</div>
<div class="container">
<div class="grid">
<div>
<div class="card">
<h2>Contracts</h2>
<div class="tabs" id="day-nav"></div>
<div id="contracts"><div class="empty">Click <b>Run Week Sim</b> to simulate a staffing week</div></div>
<div class="search-box">
<div class="search-row">
<input class="search-input" id="query" type="text" placeholder="e.g. reliable forklift operator for warehouse in Illinois" autofocus>
<button class="search-btn" id="search-btn" onclick="doSearch()">Search</button>
</div>
<div class="card">
<h2>Week Summary</h2>
<div id="week-stats"><div class="empty">No simulation data yet</div></div>
<div class="filters">
<div class="filter"><label>State:</label><select id="f-state"><option value="">Any</option><option>IL</option><option>IN</option><option>OH</option><option>MO</option><option>TN</option><option>KY</option><option>WI</option><option>MI</option></select></div>
<div class="filter"><label>Role:</label><select id="f-role"><option value="">Any</option><option>Forklift Operator</option><option>Machine Operator</option><option>Assembler</option><option>Loader</option><option>Quality Tech</option><option>Welder</option><option>Sanitation Worker</option><option>Shipping Clerk</option><option>Production Worker</option><option>Maintenance Tech</option></select></div>
<div class="filter"><label>Min Reliability:</label><input id="f-rel" type="number" min="0" max="1" step="0.1" value="0.5" style="width:50px"></div>
</div>
<div class="examples">
<span class="example" onclick="tryExample(this)">warehouse help in Illinois</span>
<span class="example" onclick="tryExample(this)">dependable machine operator with CNC</span>
<span class="example" onclick="tryExample(this)">safety trained for chemical plant</span>
<span class="example" onclick="tryExample(this)">bilingual shipping clerk</span>
<span class="example" onclick="tryExample(this)">experienced welder Ohio</span>
</div>
</div>
<div>
<div class="card">
<h2>Alerts</h2>
<div id="alerts"></div>
<div class="mode-toggle">
<button class="mode-btn active" id="mode-ai" onclick="setMode('ai')">AI Search</button>
<button class="mode-btn" id="mode-crm" onclick="setMode('crm')">CRM Keyword</button>
<button class="mode-btn" id="mode-hybrid" onclick="setMode('hybrid')">Hybrid (best)</button>
</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 id="results"></div>
<div class="footer">
<a href="proof">View Proof of Work</a> · Powered by Lakehouse · 500K workers · 673K AI-indexed chunks
</div>
</div>
</div>
<a class="proof-link" href="proof">View Proof of Work →</a>
</div>
<script type="module" src="dashboard.ts"></script>
<script>
const base = window.location.pathname.replace(/\/+$/, '');
const GW = window.location.origin + base;
let mode = 'ai';
function setMode(m) {
mode = m;
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
document.getElementById('mode-' + m).classList.add('active');
}
function tryExample(el) {
document.getElementById('query').value = el.textContent;
doSearch();
}
document.getElementById('query').addEventListener('keydown', function(e) {
if (e.key === 'Enter') doSearch();
});
async function doSearch() {
const query = document.getElementById('query').value.trim();
if (!query) return;
const state = document.getElementById('f-state').value;
const role = document.getElementById('f-role').value;
const rel = parseFloat(document.getElementById('f-rel').value) || 0.5;
const btn = document.getElementById('search-btn');
btn.disabled = true;
btn.textContent = 'Searching...';
document.getElementById('results').innerHTML = '<div class="loading">Searching ' + (mode === 'crm' ? 'by keyword' : 'with AI') + '...</div>';
const t0 = Date.now();
try {
let data;
if (mode === 'crm') {
// CRM keyword search — LIKE match
let where = "resume_text LIKE '%" + query.replace(/'/g, "''") + "%'";
if (state) where += " AND state = '" + state + "'";
if (role) where += " AND role = '" + role + "'";
where += " AND CAST(reliability AS DOUBLE) >= " + rel;
const r = await fetch(GW + '/sql', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({sql: "SELECT worker_id, name, role, city, state, skills, certifications, archetype, ROUND(CAST(reliability AS DOUBLE),2) as reliability, ROUND(CAST(availability AS DOUBLE),2) as availability FROM workers_500k WHERE " + where + " ORDER BY CAST(reliability AS DOUBLE) DESC LIMIT 10"})
});
data = await r.json();
renderCRMResults(data, query, Date.now() - t0);
} else if (mode === 'ai') {
// Pure AI vector search
const r = await fetch(GW + '/api/vectors/hnsw/search', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({index_name: 'workers_500k_v1', query: query, top_k: 10})
});
data = await r.json();
renderAIResults(data, query, Date.now() - t0);
} else {
// Hybrid — SQL filter + AI ranking
let filter = "CAST(reliability AS DOUBLE) >= " + rel;
if (state) filter += " AND state = '" + state + "'";
if (role) filter += " AND role = '" + role + "'";
const r = await fetch(GW + '/search', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
question: query, index_name: 'workers_500k_v1',
sql_filter: filter, dataset: 'workers_500k',
id_column: 'worker_id', top_k: 10, generate: false
})
});
data = await r.json();
renderHybridResults(data, query, Date.now() - t0);
}
} catch (e) {
document.getElementById('results').innerHTML = '<div class="empty">Error: ' + e.message + '</div>';
}
btn.disabled = false;
btn.textContent = 'Search';
}
function renderCRMResults(data, query, ms) {
const el = document.getElementById('results');
const rows = data.rows || [];
el.innerHTML = '';
const header = document.createElement('div');
header.className = 'result-header';
header.innerHTML = '<span class="count">' + rows.length + ' results</span>' +
'<span class="meta">CRM keyword match · "' + query + '" · ' + ms + 'ms</span>';
el.appendChild(header);
if (!rows.length) {
el.innerHTML += '<div class="empty">No exact keyword match found.<br>Try <b>AI Search</b> — it understands what you mean, not just the exact words.</div>';
return;
}
rows.forEach(function(w) { el.appendChild(makeWorkerCard(w, null)); });
}
function renderAIResults(data, query, ms) {
const el = document.getElementById('results');
const hits = data.results || [];
el.innerHTML = '';
const header = document.createElement('div');
header.className = 'result-header';
header.innerHTML = '<span class="count">' + hits.length + ' results</span>' +
'<span class="meta">AI vector search · understood meaning · ' + ms + 'ms</span>';
el.appendChild(header);
if (!hits.length) {
el.innerHTML += '<div class="empty">No results found.</div>';
return;
}
hits.forEach(function(h) {
const parts = (h.chunk_text || '').split('—');
const name = (parts[0] || '').trim();
const rest = (parts[1] || '').trim();
const roleMatch = rest.match(/^(.+?) in (.+?)\./);
const skillsMatch = rest.match(/Skills: ([^.]+)/);
const certsMatch = rest.match(/Certs?: ([^.]+)/);
const relMatch = rest.match(/Reliability: ([\d.]+)/);
const availMatch = rest.match(/Availability: ([\d.]+)/);
const archMatch = rest.match(/Archetype: (\w+)/);
const w = {
name: name, role: roleMatch ? roleMatch[1].trim() : '',
city: roleMatch ? roleMatch[2].split(',')[0].trim() : '',
state: roleMatch ? (roleMatch[2].split(',')[1] || '').trim() : '',
skills: skillsMatch ? skillsMatch[1] : '', certifications: certsMatch ? certsMatch[1] : '',
reliability: relMatch ? relMatch[1] : '', availability: availMatch ? availMatch[1] : '',
archetype: archMatch ? archMatch[1] : '', worker_id: h.doc_id
};
el.appendChild(makeWorkerCard(w, h.score));
});
}
function renderHybridResults(data, query, ms) {
const el = document.getElementById('results');
const sources = data.sources || [];
el.innerHTML = '';
const header = document.createElement('div');
header.className = 'result-header';
const badges = '<span class="badge green">' + (data.sql_matches || 0) + ' SQL matches</span> ' +
'<span class="badge purple">AI ranked top ' + sources.length + '</span> ' +
'<span class="badge blue">' + ms + 'ms</span>';
header.innerHTML = '<span class="count">' + sources.length + ' results</span><span class="meta">' + badges + '</span>';
el.appendChild(header);
if (!sources.length) {
el.innerHTML += '<div class="empty">No matches with current filters. Try broadening the state or role filter.</div>';
return;
}
sources.forEach(function(s) {
const parts = (s.chunk_text || '').split('—');
const name = (parts[0] || '').trim();
const rest = (parts[1] || '').trim();
const roleMatch = rest.match(/^(.+?) in (.+?)\./);
const skillsMatch = rest.match(/Skills: ([^.]+)/);
const certsMatch = rest.match(/Certs?: ([^.]+)/);
const relMatch = rest.match(/Reliability: ([\d.]+)/);
const availMatch = rest.match(/Availability: ([\d.]+)/);
const w = {
name: name, role: roleMatch ? roleMatch[1].trim() : '',
city: roleMatch ? roleMatch[2].split(',')[0].trim() : '',
state: roleMatch ? (roleMatch[2].split(',')[1] || '').trim() : '',
skills: skillsMatch ? skillsMatch[1] : '', certifications: certsMatch ? certsMatch[1] : '',
reliability: relMatch ? relMatch[1] : '', availability: availMatch ? availMatch[1] : '',
worker_id: s.doc_id
};
const card = makeWorkerCard(w, s.score);
if (s.sql_verified) {
const v = document.createElement('div');
v.className = 'verified';
v.textContent = '✓ SQL verified against database';
card.appendChild(v);
}
el.appendChild(card);
});
}
function makeWorkerCard(w, score) {
const card = document.createElement('div');
card.className = 'worker';
const top = document.createElement('div');
top.className = 'top';
const info = document.createElement('div');
const nameEl = document.createElement('div');
nameEl.className = 'name';
nameEl.textContent = w.name || w.worker_id || '—';
info.appendChild(nameEl);
if (w.role) { const r = document.createElement('div'); r.className = 'role'; r.textContent = w.role; info.appendChild(r); }
if (w.city || w.state) { const l = document.createElement('div'); l.className = 'location'; l.textContent = [w.city, w.state].filter(Boolean).join(', '); info.appendChild(l); }
top.appendChild(info);
if (score !== null && score !== undefined) {
const sc = document.createElement('div');
sc.className = 'score';
const num = document.createElement('div');
num.className = 'num';
num.textContent = (score * 100).toFixed(0) + '%';
sc.appendChild(num);
const label = document.createElement('div');
label.className = 'label';
label.textContent = 'AI match';
sc.appendChild(label);
top.appendChild(sc);
}
card.appendChild(top);
const details = document.createElement('div');
details.className = 'details';
if (w.skills) addDetail(details, 'Skills', w.skills.replace(/\|/g, ', '));
if (w.certifications && w.certifications !== 'none') addDetail(details, 'Certs', w.certifications.replace(/\|/g, ', '));
if (w.reliability) addDetail(details, 'Reliability', w.reliability);
if (w.availability) addDetail(details, 'Availability', w.availability);
if (w.archetype) addDetail(details, 'Type', w.archetype);
card.appendChild(details);
return card;
}
function addDetail(parent, label, value) {
const d = document.createElement('div');
d.className = 'detail';
const dt = document.createElement('div');
dt.className = 'dt';
dt.textContent = label;
d.appendChild(dt);
const dd = document.createElement('div');
dd.className = 'dd';
dd.textContent = value;
d.appendChild(dd);
parent.appendChild(d);
}
</script>
</body>
</html>

View File

@ -1,232 +1 @@
// 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);
// Replaced by inline script in dashboard.html