Dashboard¶
Flask host for the Python-generated root dashboard and incrementally migrated React dashboard, with shared same-origin APIs.
Details¶
Web dashboard for system observability + control. Served as a sidecar-managed Flask service on port 5127. Accessible remotely via Tailscale Serve.
Two frontends, one authority¶
The service hosts two distinct frontends during incremental migration:
/is the Python-generated root dashboard documented atservices/dashboard/frontend./appis the React dashboard documented atservices/dashboard/react.
The React surface is the primary desktop entry point, but it does not imply that every root-dashboard tab or mutation has moved. Both frontends use the same Flask process and same-origin API authority; neither browser may call sibling localhost service ports directly.
Tabs¶
Static: Overview, Threads, Today, Tasks, Jobs, Chats, Contracts, Projects, Costs. Plus a Settings panel reached via a gear icon in the header (off the nav bar by design — Settings is a configuration surface, not a peer of the daily-use tabs).
Dynamic: Threads, Triage, and Notifications appear via workflow views, the thread system, and the notification log.
Settings tab¶
Primary consumer of the control graph (see architecture/control-graph for the aggregator; architecture/health for the four-layer mental model the graph fuses). The Settings panel has two sub-tabs: Status — the control-graph tree (domain → subsystem → component hierarchy with effective_state badges, preference toggles (Want / No thanks / Undecided, hidden for is_core components), Configure / Walk me through action buttons for fixable requirements, universal ? help buttons that spawn interactive Claude Code sessions with structured briefs, a per-component ↻ reprobe button, and clickable bulk-state chips that jump to the first problem node of that state) — and Activity, a registry-driven set of cards (Obsidian bridge sparkline, sidecar event log, recent-notifications log). See the Card registry section below.
The Settings panel also has Embeddings and Inference sub-views. Settings › Inference hosts the per-call provenance activity feed and, above it, a Local model fleet section — one card per machine (reachability, loaded models, multi-GPU hardware), load-on-open + manual refresh, live-updated via the fleet.changed SSE event. Reads serve from GET /api/fleet (cached per-machine snapshot); the inline roster editor writes via POST /api/fleet/roster (add/update or clear a machine's inference.fleet entry; _reject_read_only-gated, mirrors /api/embeddings/vault). See architecture/inference/fleet.
Modes and endpoints¶
- Dev mode:
python -m work_buddy.dashboard --dev(auto-reloads on file changes). Not enabled in sidecar config — use manually for local development only. - Root frontend: the UI at
/is generated by Python modules underfrontend/and served as content-hashed cached assets. Seeservices/dashboard/frontendfor the module structure, concatenation order, event delegation, escaping, shared widgets, and asset serving. - React app:
GET /app/serves the Vite build fromdashboard-react/dist. It is the primary desktop UI opened bywbuddy launchand by the installer’s console-less native launcher; views move into it incrementally while unmigrated surfaces remain available in the root dashboard.GET /app/manifest.webmanifestandGET /app/icons/*provide optional PWA install metadata and branded icons with explicit media types. The PWA is an identity/standalone-window enhancement, not the runtime bootstrap. Release packaging builds this frontend before assembling the source-tree payload and refuses to build a payload when the dist is absent. - Remote access: Published privately via
tailscale serve --bg 5127— thetailscalecomponent (registered inCOMPONENT_CATALOG) gates this with click-to-fix requirements; seearchitecture/health/componentsandstatus/tailscale-status-directions. The browser only hits same-origin/api/...routes; all local service reads happen server-side. - Read-only mode:
dashboard.read_only: trueinconfig.yamlgates every mutating HTTP method (403) and hides or disables mutation controls in both frontends.
The React dashboard's standardized widget runtime, appearance contract, and calendar presentation are documented under services/dashboard/react. Registry-driven configuration authority is documented at settings.
Card registry (feature cards)¶
The Settings → Activity sub-view is registry-driven: its widgets (Obsidian bridge sparkline, sidecar event log, recent-notifications log) are DashboardCards, not hand-coded render blocks. loadActivity() calls window.wbMountCards('activity', ...), which fetches the active card list and renders each registered renderer. A card may carry a gate — a boolean expression over component-active state — so a card whose component is opted out simply does not mount (no placeholder). The bridge card is gated on the obsidian component; opting Obsidian out also stops the backend bridge probe in get_system_state(). See architecture/feature-cards for the full pattern — gate AST, registry, endpoint, and how to add a card (including from a plugin).
GET /api/dashboard/cards/<mount_point>— active card descriptors for a mount point, gates evaluated against current component preferences. Read-only.
Right-rail surface (chat sidebar)¶
The dashboard has a persistent right-side surface — wb-chat-sidebar — that slides in beside the main content, which squishes left via html { padding-right }. Hosts a conversation_chat renderer in pane mode. See services/dashboard/chat-sidebar for the full reusable API; first consumer is the Jobs tab's 💬 Help me create a job button (endpoint POST /api/user_jobs/help).
Distinct from the conversation_chat workflow-view tab — same renderer, different mount point: a workflow-view tab is a full-tab pane reached via the CHAT toast, while the chat sidebar opens directly without a toast and squishes the active tab rather than replacing it.
Agent ↔ form bridge¶
The chat sidebar (above) is the conversation surface; the form bridge is the interaction surface — schema-driven, typed, and reusable across forms. See services/dashboard/form-bridge for the full design. Agents call the single MCP capability dashboard_interact to fill fields, open the form, click submit, and read state; the dashboard validates against the form's registered FormSchema and routes events through window.wbFormBridge to per-form handlers.
Real-time updates¶
The dashboard updates in real time from server-pushed events delivered over GET /api/events (Server-Sent Events). Each event mutates only the specific row(s) it concerns; panels are never wholesale-rewritten. bus.heartbeat published every 10 s as a liveness signal. See architecture/event-bus for the full design.
Control-graph endpoints (added with the Settings tab)¶
GET /api/control/graph[?force=1]— serialized graph + cache info.POST /api/control/preference— toggle component preferences.POST /api/control/fix/<req_id>— apply a fix (programmatic / input_required / agent_handoff).POST /api/control/help/<node_id>— spawn an interactive help session.POST /api/control/reprobe— re-run every tool probe, rebuild the graph.POST /api/reprobe/<component_id>— pre-existing; per-component reprobe, reused by Settings' ↻ button.
All mutating control endpoints are gated by _reject_read_only() and auto-grant the relevant consent (the click IS the consent, same pattern as workflow-launch).
Settings broker endpoints¶
GET /api/settings/registry— definitions, pages, sections, and placements.GET /api/settings/values[?context_id=...]— authoritative effective values for a page or view context.POST /api/settings/values/<setting_id>/preview— validate and describe a proposed value without storing or publishing it.PATCH /api/settings/values/<setting_id>— revision-checked update through the setting's declared authority.DELETE /api/settings/values/<setting_id>— revision-checked reset;POST /api/settings/resetis the body-addressed compatibility form.
Settings responses are no-store. Writes and resets honor dashboard read-only mode and publish settings.changed after success. See settings for registry identity, navigation placement, authority, and persistence.
Form-bridge endpoints¶
POST /api/dashboard/interact— typed entry point for agents driving forms (called by thedashboard_interactMCP capability and any other process). Body{action, form_id, field?, value?, timeout_seconds?}.POST /api/dashboard/interact/result/<request_id>— frontend's postback for rendezvous-backed actions (form_submit,form_get_state). Body{ok, error?, errors_by_field?, fields?}.
Both gated by _reject_read_only(). See services/dashboard/form-bridge for the protocol.
User-job endpoints¶
POST /api/user_jobs— create a user-job file from the Add-job form. Same path the chat-walkthrough agent goes through (via the form bridge'ssubmitHandler), so any future change to validation or payload shape benefits both flows.POST /api/user_jobs/help— open a chat-driven walkthrough. Silently creates a conversation, fire-and-forgets a headless Claude session bound to it, returns{ok, conversation_id, title}for the frontend to feed intowbChatSidebar.open. Auto-grantssidecar:agent_spawnonce-consent inside the spawn helper.
Both gated by _reject_read_only().
Triage flow (no separate dashboard endpoints)¶
Triage runs through the unified source pipeline (run_source_pipeline capability, dispatching to EmailTriagePipeline / ChromeTriagePipeline / JournalBacklogPipeline / inline-capture). Spawned Threads land on the Threads tab for the user to approve/reject/defer per child. There is no separate Review-tab surface or Resolution-Surface endpoints — those were retired in the clarify → Threads migration. Per-cluster actions resolve via the standard Threads action-chip dispatch path.
CRITICAL for all agents modifying dashboard code¶
- Never add browser-side fetches to sibling localhost ports (5123, 5124, 27125, etc.) — these break on mobile and over Tailscale. All cross-service reads must happen server-side.
- Gate new POST routes with
_reject_read_only()so read-only deployments stay read-only. - Same-origin only for any fetch from the frontend.
- Silent conversation create for sidebar-bound chats — call
conversations.store.create_conversationdirectly, NOT theconversation_createcapability, so_notify_conversation_createddoes not double-mount the conversation as both a CHAT toast/workflow-view tab and a sidebar. - Do not subscribe to
dashboard.form.*events directly from per-tab JS. ThewbFormBridge(core/form_bridge.py) owns that event family; tab modules register handlers viawindow.wbFormBridge.register(form_id, ...). - Keep request handlers off the hot-path anti-patterns — no per-request config parse, per-open schema work, N+1 store opens, or unbounded synchronous bridge/subprocess calls. Serve expensive reads from a background-refreshed cache and pre-warm at startup. See
architecture/hot-path-discipline.