---
name: fleet-dashboard-shipped-2026-06-27
description: Fleet Dashboard at omidlab.net/dashboard — at-a-glance view of all 23 agents grouped by state (needs-you / working / done / idle). Two-tier polling (window_activity sweep + LLM classifier) for near-zero idle cost.
metadata: 
  node_type: memory
  type: project
  originSessionId: cd07777c-8add-4afc-9cb9-9113caa0331f
---

Built and live at https://omidlab.net/dashboard (auth: same password as `/agents` — `?pw=...` or prompt on first visit, then localStorage).

**Architecture:**
- Backend lives in `serve.py` (`_dash_*` helpers, routes `/api/dashboard`, `/api/dashboard/mark-active/<name>`, `/dashboard`). About 200 lines added before the `__main__` block.
- Two-tier polling: cheap `tmux list-windows -F '#{window_name} #{window_activity}'` (~5ms for all 23) → deep `capture-pane` + classifier only for agents whose `window_activity` changed since last poll, or that are in the user-active set (30 min after click/send).
- Hybrid classifier: regex pre-pass (prompt-FIRST: if `❯` line visible → idle; else spinner pattern with `…` or `for Nm Ns` suffix → working). When regex says idle or ambiguous → Haiku-4.5 call via Anthropic REST API. Key loaded from `agents_stuff/.env` or `life/projects/persian/.env`.
- Pane-hash cache keys reuse classifications across polls when content didn't change.
- Frontend polls every 10s while `document.visibilityState === 'visible'`; pauses when tab hidden.

**Critical regex lessons:**
1. Spinner regex must check the `❯` prompt FIRST. Claude Code shows a static "✻ Crunched for Ns" tag above the prompt as a leftover from the last turn — not an active spinner. If the prompt is visible at the bottom, the agent is idle regardless.
2. Spinner pattern requires `…` or ` for \d+` suffix — without it, completed tool-call lines like `● Read(file)` and `● Bash(cmd)` false-match as "working".

**Auth + secrets:**
- `AGENTS_PASSWORD` is the existing module-level constant in serve.py
- `ANTHROPIC_API_KEY` read at import time from `agents_stuff/.env` (first) or `life/projects/persian/.env` (fallback) — never logged or echoed

**Follow-ups:**
- `serve.py` is NOT in a git repo. Local on VPS at `/data/cameron/para/.agents/reports/serve.py` (path is local, NOT sshfs — verified via `df -T`). All edits live only on disk; should be moved into a tracked repo. Tag this under [[project_migrate_offlab_pending]].
- "Asking" group is high-fidelity but high-volume: each agent Cameron has a paused conversation with surfaces there. Acceptable for now (it's reality), but a "mark as acknowledged for N min" button per row would reduce noise.
- Consider SSE push when state transitions happen (not polling), so phone notifications are a one-liner later. Architecture already supports it (we know which agents changed since last sweep).

**How to apply:**
- Cameron uses this as his single pane of fleet glass — don't break the `/dashboard` or `/api/dashboard` routes when editing serve.py
- Future agents should NOT pollute pane output with the bullet-char + verb-ing patterns the regex looks for
- If polling cost ever matters: tighten the LLM call (smaller pane tail, batch multiple agents per call)

## 2026-07-08 — added `omidlab.net/yukon/*` reverse-proxy route

serve.py now includes a generic reverse proxy from `omidlab.net/yukon/<path>` to `https://yukon.tail2a8ee0.ts.net:8443/<path>` (Tailscale IP + self-signed cert; requests uses `verify=False`). Streams with Range-header forwarding for video seek. Pattern mirrors `_proxy_persian`. Reason: Cameron's phone/browser occasionally can't hit Tailscale MagicDNS URLs directly (device off tailnet, MagicDNS disabled, in-app browser bypassing system DNS), so exposing yukon-hosted content through omidlab.net gives him a working URL from anywhere.

**Gotcha (learned during this addition):** if a second `cloudflared` process is running alongside the systemd unit (e.g. left over from the 2026-07-05 `labdown` script that rewrote cad.omidlab.net's origin from lab to localhost), Argo tunnel load-balances connections between them. Requests to routes added AFTER the stray was spawned will 404 on ~half of hits. Fix: `pgrep -af cloudflared`, kill the non-systemd one, verify with `curl` retries. Also worth adding to labdown / similar scripts a note that they should NOT background-spawn cloudflared under the systemd tunnel_id — they should systemctl-restart it instead.
