CIS490/training/dashboard/__main__.py
Max Gorog a8157ed177 training/dashboard: live deck at dashboard.wg, fed by receiver
Starlette + WebSocket dashboard run on the Pi as cis490-dashboard.service
(127.0.0.1:8447, Caddy-fronted at dashboard.wg). Tails
/var/lib/cis490/index.jsonl for episode events, snapshots host counts
every 30s, broadcasts to every connected browser. New connections get a
warm snapshot (recent_episodes, total_bytes, host_counts) so reloads
don't see a cold dashboard.

Frontend is a 10-scene scrollytelling deck following the project
outline: intro, collect, hosts, db explorer, baseline, attacks,
chunking, models, knn, perf. Sticky full-bleed canvas with a
right-aligned prose column (matrix-explorable layout). Hotkeys (arrows,
space, j/k, c, Home/End), prev/next chevrons, FAB, and an opt-in
click-to-advance toggle. Demo toggle drives synthetic data for the
five scenes that have no real producer yet (attack envelopes,
chunking, model bars, knn scatter, perf scatter); when off, those
scenes show "awaiting <event_type> events" rather than fake data.

Producers wire in by POSTing typed JSON to 127.0.0.1:8447/publish
(loopback only; Caddy 404s it externally). Event types the widgets
subscribe to: model_metric {model, accuracy}, embedding {x, y, phase},
model_perf {model, latency_us, accuracy}, prediction {episode_id,
window_idx, predicted, actual}, attack_profile {name, shape, curve},
phase {phase}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 21:26:07 -05:00

46 lines
1.3 KiB
Python

from __future__ import annotations
import argparse
import logging
import os
from pathlib import Path
import uvicorn
from .app import make_app
def main() -> None:
parser = argparse.ArgumentParser(prog="cis490-dashboard")
parser.add_argument(
"--host",
default=os.environ.get("CIS490_DASHBOARD_HOST", "127.0.0.1"),
help="bind address (default 127.0.0.1; Caddy reverse-proxies dashboard.wg here)",
)
parser.add_argument(
"--port", type=int,
default=int(os.environ.get("CIS490_DASHBOARD_PORT", "8447")),
help="bind port (default 8447)",
)
parser.add_argument(
"--data-root", type=Path,
default=Path(os.environ.get("CIS490_DATA_ROOT", "/var/lib/cis490")),
help="receiver data root that the feeders read from",
)
parser.add_argument(
"--no-feeders", action="store_true",
help="don't start the on-disk feeders (useful for local dev)",
)
args = parser.parse_args()
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
app = make_app(enable_feeders=not args.no_feeders, data_root=args.data_root)
uvicorn.run(app, host=args.host, port=args.port, log_config=None)
if __name__ == "__main__":
main()