diff --git a/mcp-server/dashboard.css b/mcp-server/dashboard.css new file mode 100644 index 0000000..0842307 --- /dev/null +++ b/mcp-server/dashboard.css @@ -0,0 +1,22 @@ +* { 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 new file mode 100644 index 0000000..712930d --- /dev/null +++ b/mcp-server/dashboard.html @@ -0,0 +1,33 @@ + + + + + +Lakehouse Staffing Co-Pilot + + + +
+

Lakehouse Staffing Co-Pilot

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

Contracts

Click Run Week Sim to start
+

Week Summary

+
+
+

Alerts

+

Playbooks

+

Live Log

+
+
+ + + diff --git a/mcp-server/dashboard.ts b/mcp-server/dashboard.ts new file mode 100644 index 0000000..f9f6eca --- /dev/null +++ b/mcp-server/dashboard.ts @@ -0,0 +1,201 @@ +const GW = "http://localhost:3700"; +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, cls = "") { + const el = document.getElementById("log")!; + const div = document.createElement("div"); + const t = new Date().toLocaleTimeString(); + div.textContent = `${t} ${msg}`; + if (cls) div.className = cls; + 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"; + 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 icon = c.priority === "urgent" ? "!!" : c.priority === "high" ? "!" : ""; + const title = document.createElement("div"); + title.className = "title"; + title.textContent = `${icon} ${c.id} - ${c.client}`; + div.appendChild(title); + + 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}`; + 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(", "); + div.appendChild(workers); + } + + if (c.notes) { + const notes = document.createElement("div"); + notes.className = "meta"; + 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 = `day-btn ${i === currentDay ? "active" : ""}`; + btn.textContent = d.label; + 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 = [ + ["Total contracts", s.total_contracts], + ["Total positions", s.total_needed], + ["Filled", s.total_filled], + ["Fill rate", `${s.fill_pct}%`], + ["Emergencies", s.emergencies], + ["Handoffs", s.handoffs], + ["Playbooks", 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); + 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 warn"; + div.textContent = `${row.archetype}: ${row.cnt} workers flagged`; + el.appendChild(div); + } + const info = document.createElement("div"); + info.className = "alert info"; + info.textContent = "System: all services running"; + el.appendChild(info); + } 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 === 0) { + el.textContent = "No playbooks yet - run the simulation"; + return; + } + for (const p of pbs.slice(0, 8)) { + 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 { + el.textContent = "Could not load playbooks"; + } +} + +async function checkServices() { + try { + await fetch(GW + "/health"); + document.getElementById("svc-gw")!.className = "dot green"; + } catch { + document.getElementById("svc-gw")!.className = "dot red"; + } + try { + const r = await api("/vram"); + setText("vram-display", `VRAM: ${r.gpu?.used_mib || "?"}/${r.gpu?.total_mib || "?"} MiB`); + } 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"); + return; + } + renderDayNav(); + renderContracts(simData.days[0]); + renderWeekStats(); + addLog(`Week complete: ${simData.summary?.total_filled}/${simData.summary?.total_needed} filled`, "ok"); + } catch (e) { + addLog(`Simulation failed: ${e}`, "fail"); + } +}; + +(window as any).refresh = async function () { + addLog("Refreshing..."); + await checkServices(); + await loadAlerts(); + await loadPlaybooks(); +}; + +// Initial load +checkServices(); +loadAlerts(); +loadPlaybooks(); +setInterval(checkServices, 30000); diff --git a/mcp-server/index.ts b/mcp-server/index.ts index a572356..c596414 100644 --- a/mcp-server/index.ts +++ b/mcp-server/index.ts @@ -412,7 +412,26 @@ async function main() { return new Response(await r.text(), { status: r.status, headers: { "Content-Type": "application/json" } }); } - return err("Unknown path. Available: /health /search /sql /match /worker/:id /ask /log /playbooks /profile/:id /vram /api/*", 404); + // Dashboard UI + if (url.pathname === "/" || url.pathname === "/dashboard") { + return new Response(Bun.file(import.meta.dir + "/dashboard.html")); + } + if (url.pathname === "/dashboard.css") { + return new Response(Bun.file(import.meta.dir + "/dashboard.css"), { headers: { "Content-Type": "text/css" } }); + } + if (url.pathname === "/dashboard.ts" || url.pathname === "/dashboard.js") { + // Bun transpiles TS on the fly + const built = await Bun.build({ entrypoints: [import.meta.dir + "/dashboard.ts"], target: "browser" }); + const js = await built.outputs[0].text(); + return new Response(js, { headers: { "Content-Type": "application/javascript" } }); + } + + // Week simulation endpoint + if (url.pathname === "/simulation/run" && req.method === "POST") { + return ok(await runWeekSimulation()); + } + + return err("Unknown path. Available: / /health /search /sql /match /worker/:id /ask /log /playbooks /profile/:id /vram /context /verify /simulation/run", 404); } catch (e: any) { return err(e.message || String(e), 500); } @@ -423,3 +442,131 @@ async function main() { } main().catch(console.error); + +// ─── Week simulation engine ─── + +const ROLES = ["Forklift Operator","Machine Operator","Assembler","Loader","Quality Tech","Welder","Sanitation Worker","Shipping Clerk","Production Worker","Maintenance Tech"]; +const STATES = ["IL","IN","OH","MO","TN","KY","WI","MI"]; +const CITIES: Record = { + IL: ["Chicago","Springfield","Rockford","Peoria","Joliet"], + IN: ["Indianapolis","Fort Wayne","Evansville","South Bend"], + OH: ["Columbus","Cleveland","Cincinnati","Dayton"], + MO: ["St. Louis","Kansas City","Springfield"], + TN: ["Nashville","Memphis"], KY: ["Louisville","Lexington"], + WI: ["Milwaukee","Madison"], MI: ["Detroit","Grand Rapids"], +}; +const CLIENTS = ["Midwest Logistics","Precision Mfg","Amazon DSP","CleanSpace","AutoParts Direct","Great Lakes Steel","Heartland Foods","Summit Packaging","Cardinal Health","TechFlow Assembly","River City Plastics","Prairie Wind Energy"]; +const PRIORITIES = ["urgent","high","medium","medium","medium","low"]; +const STARTS = ["5:00 AM","6:00 AM","6:30 AM","7:00 AM","7:30 AM","8:00 AM"]; +const NOTES = [ + "Warehouse expansion — need experienced workers", + "Peak season surge — client called last night", + "2nd shift, CNC preferred", + "Chemical plant — hazmat cert MANDATORY", + "ISO audit next week — need detail-oriented workers", + "Structural welding — experienced only", + "Regular fill — ongoing contract", + "Client doubled their order", + "Night shift coverage needed", + "Replacing 2 no-shows from yesterday", +]; + +function pick(arr: T[]): T { return arr[Math.floor(Math.random() * arr.length)]; } + +async function runWeekSimulation() { + const days = ["Monday","Tuesday","Wednesday","Thursday","Friday"]; + const staffers = ["Sarah (Lead)","Mike (Senior)","Kim (Junior)"]; + const results: any[] = []; + let totalFilled = 0, totalNeeded = 0, emergencies = 0, handoffs = 0, playbookEntries = 0; + + for (let d = 0; d < days.length; d++) { + const dayLabel = days[d]; + const numContracts = 4 + Math.floor(Math.random() * 5); // 4-8 per day + const contracts: any[] = []; + const staffer = staffers[d % staffers.length]; + const handoffTo = staffers[(d + 1) % staffers.length]; + + for (let c = 0; c < numContracts; c++) { + const state = pick(STATES); + const city = pick(CITIES[state] || [state]); + const role = pick(ROLES); + const priority = pick(PRIORITIES) as string; + const headcount = priority === "urgent" ? 4 + Math.floor(Math.random() * 5) : + priority === "high" ? 3 + Math.floor(Math.random() * 3) : + 2 + Math.floor(Math.random() * 3); + const minRel = priority === "urgent" ? 0.6 : priority === "high" ? 0.75 : 0.8; + const cid = `W${d+1}-${String(c+1).padStart(3,"0")}`; + + if (priority === "urgent") emergencies++; + totalNeeded += headcount; + + // Run hybrid search + let filled = 0; + let matches: any[] = []; + try { + const filt = `role = '${role}' AND state = '${state}' AND reliability >= ${minRel}`; + const r = await api("POST", "/vectors/hybrid", { + question: `Find ${role} workers for ${pick(NOTES)}`, + index_name: "ethereal_workers_v1", + sql_filter: filt, + filter_dataset: "ethereal_workers", + id_column: "worker_id", + top_k: headcount + 2, + generate: false, + }); + matches = (r.sources || []).slice(0, headcount).map((s: any) => ({ + doc_id: s.doc_id, + name: s.chunk_text?.split("—")[0]?.trim() || s.doc_id, + score: s.score, + })); + filled = matches.length; + } catch {} + totalFilled += Math.min(filled, headcount); + + contracts.push({ + id: cid, client: pick(CLIENTS), role, state, city, + headcount, filled: Math.min(filled, headcount), priority, + start: pick(STARTS), notes: pick(NOTES), matches, + staffer, handoff_to: d < 4 ? handoffTo : null, + }); + } + + // End of day: log playbook + prepare handoff + if (d < 4) { + handoffs++; + try { + await api("POST", "/api/ingest/file?name=successful_playbooks", null); // just trigger + } catch {} + } + playbookEntries++; + + results.push({ + label: dayLabel, + staffer, + handoff_to: d < 4 ? handoffTo : null, + contracts, + filled: contracts.reduce((s: number, c: any) => s + c.filled, 0), + needed: contracts.reduce((s: number, c: any) => s + c.headcount, 0), + }); + } + + const summary = { + total_contracts: results.reduce((s, d) => s + d.contracts.length, 0), + total_needed: totalNeeded, + total_filled: totalFilled, + fill_pct: Math.round(totalFilled / Math.max(totalNeeded, 1) * 100), + emergencies, + handoffs, + playbook_entries: playbookEntries, + }; + + // Log the week to playbooks + try { + const form = new FormData(); + const csv = `timestamp,operation,approach,result,context\n"${new Date().toISOString()}","week_simulation: ${summary.total_contracts} contracts over 5 days","hybrid SQL+vector with multi-model routing","${summary.total_filled}/${summary.total_needed} filled (${summary.fill_pct}%)","${summary.emergencies} emergencies, ${summary.handoffs} handoffs"`; + form.append("file", new Blob([csv], { type: "text/csv" }), "playbook.csv"); + await fetch(`${BASE}/ingest/file?name=successful_playbooks`, { method: "POST", body: form }); + } catch {} + + return { days: results, summary }; +}