# dashboard-v2 — single-pane-of-glass command center Redesign of the legacy `claw-store serve` dashboard for the current distributed architecture. ## Design goals 1. **One URL for the whole fleet.** Hit any node's `:7700`; that node fans out to every peer via existing QUIC RPC and serves an aggregated view. No "3 browser tabs" pattern. 2. **Command-center landing page.** At-a-glance health strip + key metrics + recent-events feed. Operator answers "is anything on fire?" in <2 s. 3. **Node-detail drill-down.** Click any node in the strip → detail page with per-node metrics, timers, storage counts, journal tail. 4. **Cross-cutting content browsers.** Blobs / Tags / Refs / Snapshots aggregated across the fleet, searchable, click-through to detail. 5. **Trigger actions from the UI.** Scrub, GC, snapshot-create, pin/unpin — anything currently a CLI invocation. ## Non-goals (v2 scope) - Real-time streaming metrics beyond SSE snapshots. Prometheus stays the source of truth for graphs; this dashboard is for state + actions, not observability. - Auth beyond a shared bearer token (fleet is trust-perimeter — CA auth for the UI is future work). - Editing configs. Read + trigger, never write config. ## Backend shape Additive `/api/v2/*` alongside the legacy `/api/*` handlers so the cutover is safe: ``` /api/v2/fleet aggregated snapshot (all peers) /api/v2/node//status this-peer or remote via gossip lookup /api/v2/node//timers systemd timer state for the 4 timers /api/v2/node//journal?unit=… last N lines of journalctl /api/v2/storage/blobs?limit&offset BlobStore::list_blob_ids + summaries /api/v2/storage/tags?prefix TagStore::list (both layers) /api/v2/storage/refs?limit&offset RefStore::list unioned /api/v2/storage/snapshots SnapshotStore::list /api/v2/storage/ref-tracking?repo RefTracking::list_all /api/v2/cache/metrics router.metrics().snapshot() + gossiped /api/v2/peers gossip snapshot + last-probe route /api/v2/events SSE — fleet event stream POST /api/v2/actions/scrub POST /api/v2/actions/gc { evict_to_gb? } POST /api/v2/actions/snapshot { name } POST /api/v2/actions/pin { key, blob_id_hex } POST /api/v2/actions/unpin { key } ``` ## Aggregation model **Server-side fan-out.** When the dashboard requests `/api/v2/fleet`, the serving node walks its gossip peer list and issues a `PeerStatus` RPC to each. Results collated into one JSON. Trade-offs: - Simpler frontend (no per-peer TLS material in browser). - Backend caches results per-endpoint (5 s TTL) so 10 dashboard tabs don't cause 30 peer RPCs. - Any node can serve the dashboard — no "coordinator" single point of failure. ## Frontend shape (implementation PR follows) Routes: ``` / → CommandCenter /nodes/ → NodeDetail /storage/blobs → StorageBrowser (blobs tab) /storage/tags → StorageBrowser (tags tab) /storage/refs → StorageBrowser (refs tab) /storage/snapshots → StorageBrowser (snapshots tab) /refs/tracking → RefTracking /ops → OpsPanel (timers + actions + journal) ``` React + Vite + Tailwind, single SPA served from `/usr/share/claw-store/static-v2/`. ## Cutover plan 1. Ship v2 backend endpoints — legacy `/api/*` untouched. 2. Ship v2 frontend at `dashboard-v2/`, built to `static-v2/`. 3. `claw-store serve --v2-static-dir ` — new flag serves v2 assets at `/v2` while `/` still serves legacy for a burn-in period. 4. After burn-in: swap defaults; legacy accessible at `/legacy`. 5. Remove legacy after 30 days. ## Auth Config-driven: `[dashboard] api_token = "..."`. Bearer required for all POST endpoints; GET endpoints open on trusted-fleet networks (tailscale + LAN). If token absent → POSTs disabled entirely (read-only dashboard). Same shape as the legacy dashboard.