Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-c0re/src/dashboard_events.rs
müde a15fafb5de dashboard: surface peer questions + operator override
questions pane now shows both operator-targeted threads
(target IS NULL) and agent-to-agent threads (target = some
agent). filter chips above the list: all / @operator / @peer /
per-participant. peer rows get a mauve left rule + a 0V3RR1D3
button that POSTs the same /answer-question endpoint
(OperatorQuestions::answer already permits the operator as
answerer on any target).

wire changes: OperatorQuestions gains pending_all +
recent_answered_all; QuestionAdded + QuestionResolved events
carry target: Option<String>; emit sites drop their
target.is_none() guard. answered-history rows show the
answerer prefix so override answers are auditable at a glance.
2026-05-17 22:06:53 +02:00

148 lines
5.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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;
use crate::container_view::ContainerView;
#[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>,
},
/// A question landed in the queue. `target = None` means
/// operator-targeted (`Ask { to: None | Some("operator") }`);
/// `target = Some(<agent>)` means a peer-to-peer question. Both
/// are surfaced on the dashboard so the operator can monitor /
/// override-answer stuck threads.
QuestionAdded {
seq: u64,
id: i64,
asker: String,
question: String,
options: Vec<String>,
multi: bool,
asked_at: i64,
deadline_at: Option<i64>,
target: Option<String>,
},
/// A question was answered (operator answer, peer answer,
/// operator override on a peer thread, or ttl watchdog
/// `[expired]`). Clients move the row from pending to history.
/// `cancelled = true` when the operator dismissed via the cancel
/// button.
QuestionResolved {
seq: u64,
id: i64,
answer: String,
answerer: String,
answered_at: i64,
cancelled: bool,
target: Option<String>,
},
/// 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 },
/// One container row changed — new container appeared (post-spawn
/// finalise), an existing one flipped running/needs_update/sha,
/// etc. Clients upsert by `container.name`. Payload carries the
/// full row so cold-loaded clients and event-driven clients
/// converge on the same render.
///
/// Fired by `Coordinator::rescan_containers_and_emit`, which diffs
/// a fresh `nixos-container list`–derived snapshot against the
/// last one cached on the coordinator. Mutation sites (lifecycle
/// endpoints, actions::destroy / approve, crash_watch's poll loop)
/// call the rescan after their work lands.
ContainerStateChanged {
seq: u64,
container: ContainerView,
},
/// A container that was in the previous snapshot is gone. Clients
/// drop the row by name. Fired alongside any
/// `nixos-container destroy` (operator-driven or otherwise) on the
/// next rescan.
ContainerRemoved { seq: u64, name: String },
}