web dashboard: live expert-tier panel (VRAM/RAM/disk), one-process hosting, coli web (#126)
* Web dashboard: live expert-tier panel (VRAM/RAM/disk), static hosting, coli web command - Engine: TIERS protocol line (counts + GB for VRAM/RAM/disk experts) emitted at READY and refreshed after every served turn, computed from the live pin/LRU/CUDA state. - Server: dispatcher tracks the latest snapshot, /health exposes it as "tiers"; the built web UI (web/dist) is served from the same port (read-only, traversal-safe, SPA fallback) so the dashboard needs no second process. - Web: tier bar + legend in the Runtime panel showing where the 19k experts live right now, with GB per tier; updates with the existing health polling. - CLI: `coli web` = serve + auto-open the browser once /health answers (the 744B load takes minutes; a poller waits for it), --no-browser to disable, friendly hint when web/dist isn't built. * coli web: HERE is a path string, not a Path * web: same-origin default + auto-connect when served by the engine The page hosted by coli web talked to http://127.0.0.1:8000/v1 by default while being loaded from http://localhost:8000 - a different origin, so the very first probe died on CORS ('Failed to fetch'). When the page is engine-served the default (and a stored stale factory default) now resolve to window.location.origin + /v1, and the console auto-probes on load; the Vite dev server keeps the classic default. The server's own port is also whitelisted in DEFAULT_CORS_ORIGINS for the localhost/127.0.0.1 cross-name case. * web: fix auto-connect effect returning Promise (React 19 StrictMode crash) The async connect() call inside useEffect returned a Promise that React 19 StrictMode stored as the effect cleanup. On re-render, React called destroy_() on that Promise — 'not a function'. Block body with explicit return undefined prevents any non-function from leaking into the cleanup slot. * web: rich streaming metrics — live token counter, tok/s gauge, TTFT, session totals During generation: a flashing Zap badge counts tokens in real time. After: Gauge shows tok/s, Timer shows TTFT (time to first token), Layers shows prompt→completion token counts, Clock shows queue wait. Session totals (cumulative prompt + completion) appear in the Runtime panel. All with lucide icons and tabular-nums for stable layout. Tier bar gains a smoother cubic-bezier transition and slightly taller height for visual weight. * web: hardware environment panel — CPU model, GPU count/VRAM, RAM total/free, cores Engine emits HWINFO protocol line at startup (CPU name from /proc/cpuinfo, core count, RAM total/available from /proc/meminfo, GPU count + VRAM from CUDA). Server parses and caches it, /health exposes as 'hwinfo'. Dashboard renders it with icons at the top of the Runtime section so the user sees immediately what the engine is running on. * web: downgrade to React 18 — React 19 effect cleanup bug crashes the dashboard React 19 treats the return value of every useEffect callback as a cleanup function. In practice, any async interaction (streamChat's rapid onDelta re-renders, the health poller, auto-connect) caused React 19 to store a Promise as inst.destroy and crash with 'destroy_ is not a function' on the next commit cycle. The bug reproduced in incognito, without StrictMode, and with every effect converted to explicit block bodies returning undefined — it is a React 19 regression in the passive-effect unmount path. React 18 does not exhibit this behavior. Pin to React 18 until React 19 is fixed or the codebase migrates to a pattern React 19 handles correctly. All 17 vitest pass, ErrorBoundary retained. * web: Brain page — the expert cortex, live A 76x256 canvas grid, one cell per expert (19,456 total): colour = tier (green VRAM / blue RAM / grey disk), brightness = routing heat (log-bucketed .coli_usage counts), and a white pulse that flashes on every expert routed in the current turn and decays — you watch the model think. - Engine: EMAP protocol line (1 byte/expert: 2bit tier + 6bit heat, hex) at READY and after each turn; HITS bitmap of this turn's routed experts (set where eusage increments, cleared on emit). - Server: parses both, GET /experts serves {rows, cols, map, hits, seq}. - Web: Brain tab next to Chat; canvas renderer with rAF pulse decay; hover tooltip shows layer/expert/tier/heat plus an honest depth-role heuristic (early = surface features ... late = output shaping, MTP row labelled as the speculative draft head). * web: Brain page responsive — cells sized from container via ResizeObserver Cell size derives from the wrapper's actual client box instead of fixed 1400x900, re-rendering on resize; mobile media query tightens padding, legend and tooltip. The cortex now fills whatever screen it gets.
This commit is contained in:
@@ -86,3 +86,63 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-
|
||||
}
|
||||
@media (max-width: 560px) { .sidebar { grid-template-columns: 1fr; }.brand-row, .sidebar-foot { grid-column: auto; }.empty-state h2 { font-size: 36px; }.message-list { width: calc(100% - 28px); }.composer-foot > span { display: none; } }
|
||||
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; } }
|
||||
|
||||
/* expert tier pyramid: where the 19k experts live right now */
|
||||
.tier-panel { display: grid; gap: 7px; }
|
||||
.tier-bar { display: flex; height: 10px; border-radius: 6px; overflow: hidden; border: 1px solid var(--border); background: var(--card); }
|
||||
.tier-bar span { display: block; min-width: 0; transition: width .6s ease; }
|
||||
.tier-vram { background: var(--primary); }
|
||||
.tier-ram { background: #5a9bd8; }
|
||||
.tier-disk { background: #3a4750; }
|
||||
.tier-legend { display: flex; gap: 12px; font-size: 10px; color: #839197; flex-wrap: wrap; }
|
||||
.tier-legend span { display: flex; align-items: center; gap: 5px; }
|
||||
.tier-legend i { width: 8px; height: 8px; border-radius: 2px; display: inline-block; }
|
||||
.tier-legend strong { font: 600 12px ui-monospace, SFMono-Regular, monospace; color: var(--foreground); }
|
||||
.tier-legend small { color: var(--muted-foreground); }
|
||||
|
||||
/* ---- rich metrics badges & animations ---- */
|
||||
.badge-live { background: rgba(78,214,165,.15); border-color: rgba(78,214,165,.4); color: #4ed6a5; }
|
||||
.badge-speed { background: rgba(90,155,216,.12); border-color: rgba(90,155,216,.35); color: #5a9bd8; }
|
||||
.flash { animation: flash 0.6s ease infinite alternate; }
|
||||
@keyframes flash { from { opacity: 1; } to { opacity: 0.3; } }
|
||||
.top-actions { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||||
.top-actions .badge-live, .top-actions .badge-speed { font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* session stats row */
|
||||
.session-stats { display: flex; align-items: center; gap: 6px; font-size: 10px; color: #7f8d92; padding: 2px 0; }
|
||||
.session-stats strong { font: 600 11px ui-monospace, SFMono-Regular, monospace; color: var(--foreground); }
|
||||
|
||||
/* tier bar tweaks */
|
||||
.tier-bar { height: 12px; }
|
||||
.tier-bar span { transition: width 0.8s cubic-bezier(.4,0,.2,1); }
|
||||
.tier-legend strong { font-size: 13px; }
|
||||
|
||||
/* hardware info panel */
|
||||
.hw-panel { display: grid; gap: 5px; padding: 8px 0; border-bottom: 1px solid var(--border); margin-bottom: 8px; }
|
||||
.hw-row { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--foreground); }
|
||||
.hw-row svg { color: var(--primary); flex-shrink: 0; }
|
||||
.hw-row small { color: var(--muted-foreground); margin-left: 4px; }
|
||||
|
||||
/* ---- Brain page ---- */
|
||||
.view-tabs { display: flex; gap: 4px; background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 3px; }
|
||||
.view-tabs button { display: flex; align-items: center; gap: 6px; padding: 5px 14px; font-size: 12px; font-weight: 600; color: var(--muted-foreground); background: none; border: none; border-radius: 7px; cursor: pointer; }
|
||||
.view-tabs button.active { background: var(--primary); color: #08110d; }
|
||||
.brain-page { flex: 1; display: flex; flex-direction: column; gap: 12px; padding: 18px 22px; overflow: auto; }
|
||||
.brain-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.brain-legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: 11px; color: #839197; align-items: center; }
|
||||
.brain-legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; margin-right: 5px; vertical-align: -1px; }
|
||||
.brain-pulse-hint { color: var(--primary); }
|
||||
.brain-canvas-wrap { flex: 1; overflow: auto; border: 1px solid var(--border); border-radius: 12px; background: #07090a; padding: 10px; }
|
||||
.brain-canvas-wrap canvas { image-rendering: pixelated; max-width: 100%; cursor: crosshair; }
|
||||
.brain-tip { position: fixed; z-index: 50; background: #0d1214; border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; font-size: 11px; color: var(--foreground); pointer-events: none; max-width: 280px; box-shadow: 0 8px 24px rgba(0,0,0,.5); display: grid; gap: 4px; }
|
||||
.brain-tip-title { display: flex; align-items: center; gap: 5px; font-weight: 700; color: var(--primary); }
|
||||
.brain-tip-role { color: #8b9aa3; font-style: italic; line-height: 1.4; border-top: 1px solid var(--border); padding-top: 5px; margin-top: 2px; }
|
||||
|
||||
/* Brain responsive */
|
||||
.brain-canvas-wrap { display: flex; align-items: flex-start; justify-content: center; min-height: 300px; }
|
||||
@media (max-width: 900px) {
|
||||
.brain-page { padding: 10px; }
|
||||
.brain-head { flex-direction: column; align-items: flex-start; }
|
||||
.brain-legend { gap: 8px; font-size: 10px; }
|
||||
.brain-tip { max-width: 220px; font-size: 10px; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user