# Dashboard request — scenes 7, 8, 12 visibility fixes **Audience:** dashboard session (owns `training/dashboard/`). **Producer side (this session):** * `training/producers/multi_model_metrics.py` — publishes `ModelMetric` and `ModelPerf` for **gbt, mlp, cnn, knn_semi, gru, lstm, bert** (every 5 s) * `training/producers/knn.py stream` — publishes `ModelMetric`+ `ModelPerf` for **knn** * Lambda-side `scripts/lambda-live-detection-loop.py` — publishes `LiveDetection` **and now also `Prediction`** events per inference window All confirmed delivering (`{"delivered":N}` from `/publish`). Visibility issues are all in `training/dashboard/static/dashboard.js`. The user has flagged this twice now: scene 7 (chunking) and scene 9 (model bars) are not showing real-data state in deck mode. The events exist; the widgets just don't render them. **This is the blocker for the talk.** --- ## Scene 7 — chunking timeline (`#chunk-row`) **Problem.** Cells are only built inside `buildExample()`, which is wired to `demo_start`. The `prediction` handler can only update existing cells: ```js on('prediction', m => { if (typeof m.window_idx !== 'number') return; const cells = rowEl.querySelectorAll('.chunk-cell'); const cell = cells[m.window_idx]; if (!cell) return; // ← always falls through if no demo ... }); ``` If a real `prediction` event arrives without `demo_start` having fired first, `cells.length === 0` and the event is silently dropped. **Why we can't just publish `demo_start` from this side.** It has destructive side-effects on other scenes: scene-9 (KNN scatter) loads synthetic data on `demo_start`, scene-attack profile loads synthetic curves on `demo_start`, etc. We tried this once and clobbered the live KNN scatter. **Fix request.** Lazy cell-build inside the `prediction` handler when no cells exist yet: ```js on('prediction', m => { if (typeof m.window_idx !== 'number') return; if (rowEl.children.length === 0 || rowEl.querySelector('.chunk-empty')) { // Build N empty cells on first prediction. Width grows lazily. rowEl.innerHTML = ''; ruleEl.innerHTML = ''; axisEl.innerHTML = ''; } // Ensure cell at index exists; pad with empty cells up to window_idx. let cells = rowEl.querySelectorAll('.chunk-cell'); while (cells.length <= m.window_idx) { const c = document.createElement('div'); c.className = 'chunk-cell'; c.textContent = ''; rowEl.appendChild(c); ruleEl.appendChild(Object.assign( document.createElement('div'), { className: 'tick' })); const t = document.createElement('span'); t.textContent = `${cells.length * 10}s`; axisEl.appendChild(t); cells = rowEl.querySelectorAll('.chunk-cell'); } const cell = cells[m.window_idx]; const phase = m.predicted || m.actual; if (!phase) return; cell.className = `chunk-cell ${phase}`; cell.textContent = phase.replace('_', ' '); }); ``` This keeps `demo_start`/`demo_stop` working and additionally lights up the row from real `prediction` events. If the Lambda producer re-runs episodes from window 0, you may also want a reset on `prediction` events with `window_idx === 0` (clear all cells, rebuild fresh). We can publish a `prediction_reset` event too if you'd prefer an explicit signal — let us know. --- ## Scene 8 — model accuracy bars (`.model-row`) **Problem.** The bar fill formula compresses to nothing for any F1 < 0.5: ```js const visiblePct = Math.max(0, Math.min(1, (acc - 0.5) / 0.5)) * 100; ``` Our trained models on the cross-device test split honestly land in 0.30–0.55 range (this is the **point** of held-out-by-host evaluation — real generalization is hard). With the current scale, ≥ half the bars render as 0% wide and look like there's no data flowing. **Fix request.** Either: (a) Use the full 0–1 range so a 0.35-F1 bar is still visibly 35% filled: ```js const visiblePct = Math.max(0, Math.min(1, acc)) * 100; ``` (b) Or add the numeric F1 next to the empty-looking bars (we already publish it in `accuracy`); the right-hand `.model-acc` element does already render `acc.toFixed(3)` so this may already be readable — verify that's still being shown when fill is 0%. We strongly prefer (a). Hiding 0.30-F1 models behind a 0% bar tells the user "no data" when the truth is "the model is honestly not great under cross-host generalization." That's the headline finding. --- ## Scene 12 — accuracy vs inference cost scatter **Problem A: y-axis range.** y is clamped to `[0.7, 1.0]` (or similar high range). Every model with F1 < 0.7 stacks on the bottom edge. **Fix.** Open the y-axis to `[0.0, 1.0]` (or auto-fit to the published range with a small margin). The chart's whole point is "model honesty under cross-device shift" — letting bad models show as bad is the right answer. **Problem B: overlapping labels.** Multiple points at the same y-coordinate (especially when stacked at the floor) draw their model name labels on top of each other. We've already shortened the displayed names producer-side (`gbt-O`, `mlp-R`, `knns-O`, `trf-R`, etc., max 6 chars). That helps but doesn't fully solve it when 5+ points cluster. **Fix request, pick whichever is easiest:** 1. Skip label rendering when point density is high (only label points that are local extrema, e.g. best F1, lowest latency, or non-Pareto-dominated points). 2. Offset overlapping labels with a force layout (`d3-force` style) or even just a fixed alternating up/down/left/right pattern. 3. Show labels only on hover, with a small dot-only render at rest. Option (3) is the cleanest visually and matches how most real "model zoo" scatters render in papers. --- ## Verification after dashboard JS lands Producer side keeps publishing on these channels (already running on the Pi + Lambda): - `prediction` (scene 7) — once Lambda producer is re-pointed at scene 7 events, see request below - `model_metric` + `model_perf` (scenes 8, 12) — every 30 s from `multi_model_metrics.py` on the Pi - `live_detection` (scene-live) — continuously from Lambda Open the dashboard, watch each scene. Empty-state placeholders should disappear within ~30 s of page load. --- ## Side note for scene 7 — currently no `prediction` events flow The Lambda producer (`live_detection_loop_v2.py`) currently emits `live_detection` events for the scene-live swim lanes. If you want scene 7 lit up with the same data, we can mirror per-window output to the `prediction` event type as well — say the word and we'll add a second emit. Doing that without the lazy-cell-build above accomplishes nothing on the dashboard, so let us wait on this until the JS lands.