hyperhive/hive-c0re/src/dashboard_events.rs

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 },
}