# training/dashboard/

Live web display served at `https://dashboard.wg`. A Starlette app
on `127.0.0.1:8447` behind Caddy; messages from Python are pushed to
connected browsers over a WebSocket.

This is intentionally a **blank slate** — the default page just
appends every received JSON message to a scrolling log. Build the
real widgets on top of `window.dashboard.onMessage`.

## Run locally

```sh
uv run python -m training.dashboard
# → open http://127.0.0.1:8447
```

## Push live data from Python

**Same process** (e.g. a notebook driving the page):

```py
from training.dashboard.app import broadcaster
await broadcaster.publish({"type": "metric", "name": "loss", "value": 0.42})
```

**Different process** (orchestrator, receiver, ad-hoc shell):

```sh
curl -s http://127.0.0.1:8447/publish \
     -H 'content-type: application/json' \
     -d '{"type":"hello","msg":"from cron"}'
```

The `/publish` endpoint is loopback-only (403 otherwise) and is **not**
reverse-proxied by Caddy, so it cannot be hit from the WG mesh.

## Producing events from your own code

If you're writing code that should drive a dashboard widget (model
inference, training-loop metrics, profile envelopes), see
[`PRODUCERS.md`](./PRODUCERS.md) — it documents every event type
the widgets subscribe to, the loopback-only `/publish` contract, the
reconnect gotcha, and the systemd hardening that constrains
producers running on the Pi. There's a stdlib-only Python helper at
[`client.py`](./client.py):

```python
from training.dashboard.client import Publisher
Publisher().publish({"type": "model_metric", "model": "lstm", "accuracy": 0.928})
```

## Customizing the page

The default `static/index.html` exposes `window.dashboard`:

```js
window.dashboard.onMessage = (msg) => {
  if (msg.type === 'metric') updateChart(msg.name, msg.value);
};
window.dashboard.send({type: 'request-snapshot'});  // browser → server
```

Override `onMessage` to dispatch to your own widgets. The blank-slate
log renderer is just there so a fresh deploy is observably alive.

## Deploying to the Pi

1. `sudo cp etc/cis490-dashboard.service /etc/systemd/system/`
2. `sudo cp training/dashboard/dashboard.caddy /etc/caddy/Caddyfile.d/`
3. `sudo systemctl daemon-reload && sudo systemctl enable --now cis490-dashboard.service`
4. `sudo systemctl reload caddy`
