//! Unified dashboard event channel. //! //! Anything the browser wants to react to in near-real-time flows through //! `Coordinator.dashboard_events`. Each event is stamped with a monotonic //! per-process `seq` so the client can dedupe its buffered live traffic //! against snapshot/history responses (drop frames with //! `seq <= snapshot.seq`). //! //! Why one channel instead of one-per-domain: browsers cap concurrent //! SSE connections per origin (~6 in chrome) and dispatch-by-kind on the //! client is a one-liner. Splits get reserved for high-volume sub-streams //! that most consumers don't care about (none yet). //! //! Message-broker traffic (`Sent` / `Delivered`) lives on this channel //! too. A background forwarder task in `main.rs` subscribes to the broker //! and re-emits each `MessageEvent` as a `DashboardEvent::Sent` / //! `DashboardEvent::Delivered` with a freshly-stamped seq. Keeping the //! broker's intra-process channel separate avoids coupling the broker //! (used by `recv_blocking` inside the harness loop) to dashboard //! presentation concerns. //! //! New mutation kinds (approval added/resolved, question added/answered, //! transient changed, etc.) land here as additional variants. The client //! dispatches by `kind` and updates the relevant section. use serde::Serialize; #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "snake_case", tag = "kind")] pub enum DashboardEvent { /// Broker `Sent` event mirrored onto the dashboard channel. Sent { seq: u64, from: String, to: String, body: String, at: i64, }, /// Broker `Delivered` event mirrored onto the dashboard channel. Delivered { seq: u64, from: String, to: String, body: String, at: i64, }, /// A new approval landed in the pending queue. Payload carries /// enough to render the dashboard row without a `/api/state` /// refetch (`diff` is the raw unified diff text, same shape the /// snapshot ships). /// /// The approval's own kind (`"apply_commit"` / `"spawn"`) lives on /// `approval_kind` rather than `kind` because the latter is taken /// by the serde tag identifying which `DashboardEvent` variant /// this is. ApprovalAdded { seq: u64, id: i64, agent: String, approval_kind: &'static str, sha_short: Option, diff: Option, description: Option, }, /// A pending approval transitioned to a terminal state /// (approved / denied / failed). Clients move the row out of the /// pending list and into history. ApprovalResolved { seq: u64, id: i64, agent: String, approval_kind: &'static str, sha_short: Option, /// `"approved"` / `"denied"` / `"failed"`. status: &'static str, resolved_at: i64, note: Option, description: Option, }, /// An operator-targeted question landed in the queue /// (`Ask { to: None | Some("operator") }`). Peer-to-peer /// questions (target = Some()) never fire this event — /// the dashboard only ever shows operator-bound questions, so /// the emit site filters on `target.is_none()`. QuestionAdded { seq: u64, id: i64, asker: String, question: String, options: Vec, multi: bool, asked_at: i64, deadline_at: Option, }, /// An operator-targeted question was answered (operator answer, /// peer override, or ttl watchdog `[expired]`). Clients move the /// row from pending to history. `cancelled = true` when the /// operator dismissed via the cancel button — same code path on /// the server but useful to surface differently in the UI. QuestionResolved { seq: u64, id: i64, answer: String, answerer: String, answered_at: i64, cancelled: bool, }, /// A lifecycle action started for an agent (spawn / start / stop /// / restart / rebuild / destroy). Clients render a spinner next /// to the row; the client computes "seconds in this state" /// locally from `since_unix` so a slow rebuild's elapsed time /// ticks without polling. TransientSet { seq: u64, name: String, /// Lifecycle kind: `"spawning"` / `"starting"` / `"stopping"` / /// `"restarting"` / `"rebuilding"` / `"destroying"`. transient_kind: &'static str, since_unix: i64, }, /// The matching lifecycle action resolved (success or failure). /// Clients drop the spinner row. TransientCleared { seq: u64, name: String }, }