Skip to content

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 at services/dashboard/frontend.
  • /app is the React dashboard documented at services/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 under frontend/ and served as content-hashed cached assets. See services/dashboard/frontend for the module structure, concatenation order, event delegation, escaping, shared widgets, and asset serving.
  • React app: GET /app/ serves the Vite build from dashboard-react/dist. It is the primary desktop UI opened by wbuddy launch and 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.webmanifest and GET /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 — the tailscale component (registered in COMPONENT_CATALOG) gates this with click-to-fix requirements; see architecture/health/components and status/tailscale-status-directions. The browser only hits same-origin /api/... routes; all local service reads happen server-side.
  • Read-only mode: dashboard.read_only: true in config.yaml gates 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/reset is 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 the dashboard_interact MCP 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's submitHandler), 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 into wbChatSidebar.open. Auto-grants sidecar:agent_spawn once-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_conversation directly, NOT the conversation_create capability, so _notify_conversation_created does 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. The wbFormBridge (core/form_bridge.py) owns that event family; tab modules register handlers via window.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.

Children