# Dashboard request — sticky cache for slowly-changing event types

**Audience:** dashboard session (owns `training/dashboard/`).
**Producer side:** `training/producers/multi_model_metrics.py`
(scenes 9 + 12), `training/producers/knn.py stream` (scene 11),
Lambda-side `live_detection_loop_v2.py` (scene 13).

## Problem

The broadcaster fans events out to **currently-connected** browsers
only. Reconnects (page refresh, second tab opening, mid-talk page
reload) see empty widgets until the next producer tick rebroadcasts.
The user has explicitly flagged this as a bug:

> "Your functions need to be more stateful, when we call your data it
> needs to be available right away. For the streaming data, when we
> call a new page it needs to connect correctly."

The broadcaster already does sticky caching for some keys — its
`/healthz` reports cached state under `host_counts`, `phase_mix`,
`recent_episodes`, `total_alerts`, `total_bytes`, `total_episodes`.
What's missing is sticky caching for the model + scatter + embedding
event types.

## Producer-side band-aid (already in place)

We've shortened the multi_model_metrics tick from 20 s → **5 s** so
worst-case-stale-on-reconnect drops to ~5 s. That's acceptable for
the talk but not the right architecture — at 5 s × 4 events × 2
event types we're spending bandwidth and CPU on retransmits the
broadcaster could just remember.

## Asks

Please add sticky caching to the broadcaster for these event types:

| event type        | scene | key                | TTL   | replay-on-connect? |
|-------------------|-------|--------------------|-------|---------------------|
| `model_metric`    | 9     | one entry per `model` (last value wins) | none  | yes |
| `model_perf`      | 12    | one entry per `model` (last value wins) | none  | yes |
| `live_detection`  | 13    | a small ring buffer, e.g. last 60 events globally (or last 12 per host_id) | none | yes |
| `embedding`       | 11    | one snapshot — see companion request `dashboard-request-knn-cap-evict.md` for the snapshot-replace pattern | none | yes |
| `attack_profile`  | 7     | one entry per `name` (last curve wins) | none | yes |
| `prediction`      | 8     | one entry per `(episode_id, window_idx)` last value wins | none | yes |

Implementation suggestion: extend the broadcaster's existing
state-keys cache with a per-event-type "sticky map." On new client
connect, replay the cache before any live event reaches the new
client.

For `live_detection` the right structure is a ring-buffer (60 cells
per lane match the widget's DOM cap; replaying 60 newest events lets
a new browser paint the lanes immediately).

## Verification

After this lands, our producers can drop their republish cadence
back to a sane 30 s + on-change-only, and a cold page-load on
`dashboard.wg` paints scenes 9, 11, 12, 13 within one frame.

We'll also drop the 5 s tick on `multi_model_metrics` once we
verify replay works.
