124 lines
4.6 KiB
Rust
124 lines
4.6 KiB
Rust
//! 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<String>,
|
|
diff: Option<String>,
|
|
description: Option<String>,
|
|
},
|
|
/// 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<String>,
|
|
/// `"approved"` / `"denied"` / `"failed"`.
|
|
status: &'static str,
|
|
resolved_at: i64,
|
|
note: Option<String>,
|
|
description: Option<String>,
|
|
},
|
|
/// An operator-targeted question landed in the queue
|
|
/// (`Ask { to: None | Some("operator") }`). Peer-to-peer
|
|
/// questions (target = Some(<agent>)) 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<String>,
|
|
multi: bool,
|
|
asked_at: i64,
|
|
deadline_at: Option<i64>,
|
|
},
|
|
/// 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 },
|
|
}
|