Skip to content

Dashboard Event Bus

In-process pub/sub + SSE stream + localhost cross-process ingress that powers real-time dashboard updates without a global panel-refresh timer.

Details

Why

The dashboard updates in real time from server-pushed events delivered over Server-Sent Events. Each event mutates only the specific DOM nodes it concerns; a panel is never wholesale-rewritten in response to an event. This preserves user state — focused inputs, scroll position, drilled-in <details>, chip rails — across every update.

Relationship to the durable Events backbone

This bus is the lossy real-time UI layer — drop-oldest, no durability, no dedup — and is not the durable delivery spine. The first-class events backbone (work_buddy.events) owns at-least-once, deduped, offset-tracked delivery of event-shaped facts to reacting consumers; it uses this bus only as its immediate, best-effort UI projection (its publish() fans out here via publish_auto). Rule of thumb: reliable reactions read the durable events log; live UI reads this bus.

Surfaces

  • Python
  • work_buddy.dashboard.events.EventBus — thread-safe in-process pub/sub with per-subscriber bounded deque + threading.Condition.
  • events.publish(event_type, payload) — in-process publish.
  • events.publish_cross_process(event_type, payload) — POSTs {event_type, payload} to the dashboard's loopback /internal/bus endpoint.
  • events.publish_auto(event_type, payload) — routes by process flag (mark_dashboard_process() set in service.main()).
  • events.start_heartbeat(interval, bus) — publishes bus.heartbeat every interval seconds (default 10 s).
  • work_buddy.clarify.capabilities.triage_review_pool.compose_entry_presentation_group(entry) — single-entry presentation composer used by ClarifyPool.submit / submit_raw for fat-add events.

  • HTTP

  • GET /api/events — SSE stream. No read-only gate. Cache-Control: no-cache, X-Accel-Buffering: no. 15 s idle keepalive comment to defeat intermediary idle-close.
  • POST /internal/bus — loopback-only ingress for cross-process publishers. Validates event_type and re-publishes {event_type, payload} on the in-process bus. Gated to 127.0.0.1 / ::1; exempt from the read-only gate (UI-refresh events must flow even in display-only mode).

  • Browser

  • window.eventBus.{on, off, isConnected, lastHeartbeat} — per-event-type dispatcher.
  • window.<panel>Surface — per-panel handle (Review / Tasks / Settings / Costs / Jobs / Projects).
  • Connection-status dot at #event-bus-status in the dashboard header.
  • The React dashboard owns one EventSource through its event provider. Consumers subscribe through that provider; individual widgets do not open their own SSE connections.

Vendored dependency

frontend/vendor/morphdom-umd.min.js v2.7.4 (12 KB, MIT) — the surgical-update primitive used by Phoenix LiveView and Hotwire/Turbo.

Event taxonomy

Event Payload Publisher
bus.heartbeat {interval} events.start_heartbeat (every 10 s)
pool.entry_added {run_id, item_id, source, adapter, raw?, group} (fat) ClarifyPool.submit / submit_raw
pool.entry_state_changed {run_id, item_id, state, reason?, outcome?} ClarifyPool.mark_state / mark_reviewed (and the wrappers)
pool.attraction_passes_bumped {run_id, item_id, count} ClarifyPool.increment_attraction_pass
pool.forced_context_stored {run_id, item_id} ClarifyPool.store_forced_context
task.created {task_id, state, urgency, contract} tasks.mutations.create_task
task.state_changed {task_id, state, reason} update_task (when state is set) + toggle_task
task.description_changed {task_id, description} update_task_description
project.created {project_id, slug, status, author} projects.store.upsert_project (new row)
project.updated {project_id, slug, author} projects.store.upsert_project (existing row), update_project
project.deleted {project_id, slug, author} projects.store.delete_project (soft-delete)
project.folders_changed {project_id, action, path, author} (action: add | remove | archive | unarchive) projects.store.add_folder / remove_folder / set_folder_archived
project.aliases_changed {project_id, action, alias, author} (action: add | remove) projects.store.add_alias / remove_alias
project.description_confirmed {project_id, revision_id} projects.store.confirm_description
component.health_changed {component_id, available, reason} tools.probe_all / reprobe_one (transition-only)
component.preference_changed {component_id, wanted, reason} health.preferences.apply_preference_updates
llm.call_logged {model, task_id, input_tokens, output_tokens, estimated_cost_usd, execution_mode, cached} llm.cost.log_call
cron.hot_reload {old_count, new_count} Scheduler._hot_reload (when fingerprints change)
user_job.created {name, file_path} api_user_job_create (after successful write)
inference.call_logged {call_id, description, kind, model, execution_mode, status} (thin ping) llm.provenance.record_inference_call — Settings › Inference refetches the cached /api/inference-activity
fleet.changed {reason} dashboard.api.start_fleet_poller (25 s) — the Settings › Inference fleet section refetches the cached /api/fleet and morphs the cards. Published only when a machine's reachability or loaded-model set changes (external LM Studio loads/unloads have no other internal event).
settings.changed {setting_id, scope_id, revision, effective_state} Settings broker after a successful server/profile mutation or reset. React Settings and view contexts reconcile the affected definition/value.

Replay semantics

The in-process bus does not replay events from before a subscriber registered. Cross-process events are delivered by a live POST to /internal/bus with no durable buffer, so an event fired while the dashboard is down — or before the browser's EventSource connects — is simply not seen. The browser sees events from the in-process bus only AFTER its EventSource connects. The browser's visibilitychange listener refreshes the active tab when it returns to foreground after a backgrounded period.

Refresh patterns per surface

Most surfaces use window._wbMorphReplace to merge fresh server-rendered HTML into the live container surgically — user state (focused inputs, scroll, drilled-in <details>) is preserved natively by morphdom. The projectsSurface currently uses a full refetch + re-render of the project list sidebar (the detail pane is untouched, so per-card inline edits are preserved). Upgrading projects to morphdom-merge is a planned follow-up.

The React dashboard does not mutate root-dashboard DOM nodes. Its singleton event provider distributes typed events to React consumers, which reconcile only the affected domain state. The transport remains lossy: durable effects and retries never depend on an SSE message being observed.

Operational notes

  • SSE survives Werkzeug --dev and Tailscale Serve.
  • Multi-worker deployment is out of scope (each worker has its own bus; an external broker would be required).
  • Slow subscribers drop oldest events with an exposed counter rather than blocking publishers (per-subscriber deque capped at 1000).