dashboard events: unified coord channel + /dashboard/{stream,history}; broker forwards
This commit is contained in:
parent
d48cee7c2d
commit
a478792914
6 changed files with 205 additions and 66 deletions
|
|
@ -57,8 +57,8 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
.route("/request-spawn", post(post_request_spawn))
|
||||
.route("/op-send", post(post_op_send))
|
||||
.route("/meta-update", post(post_meta_update))
|
||||
.route("/messages/stream", get(messages_stream))
|
||||
.route("/messages/history", get(messages_history))
|
||||
.route("/dashboard/stream", get(dashboard_stream))
|
||||
.route("/dashboard/history", get(dashboard_history))
|
||||
.route("/static/hive-fr0nt.js", get(serve_shared_js))
|
||||
.with_state(AppState { coord });
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
|
|
@ -73,7 +73,7 @@ pub async fn serve(port: u16, coord: Arc<Coordinator>) -> Result<()> {
|
|||
// (static) shell; `GET /static/*` serves the CSS + JS app; `GET /api/state`
|
||||
// returns the current snapshot as JSON. The JS app fetches state on load,
|
||||
// re-fetches after every async-form submit, and listens on
|
||||
// `/messages/stream` for broker traffic.
|
||||
// `/dashboard/stream` for the unified live event channel.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `SO_REUSEADDR` bind with retry. Mirrors the per-agent variant —
|
||||
|
|
@ -293,13 +293,13 @@ async fn api_state(headers: HeaderMap, State(state): State<AppState>) -> axum::J
|
|||
.unwrap_or("localhost");
|
||||
let hostname = host.split(':').next().unwrap_or(host).to_owned();
|
||||
|
||||
// Capture the broker seq *before* any read so the dedupe contract
|
||||
// is "events with seq > snapshot.seq are post-snapshot, never
|
||||
// missed." A broker event landing during snapshot construction may
|
||||
// be doubly applied (snapshot caught the write + client also
|
||||
// applies the SSE event) — that's a renderer's problem to make
|
||||
// idempotent, not ours to avoid here.
|
||||
let seq = state.coord.broker.current_seq();
|
||||
// Capture the unified dashboard-channel seq *before* any read so the
|
||||
// dedupe contract is "events with seq > snapshot.seq are
|
||||
// post-snapshot, never missed." An event landing during snapshot
|
||||
// construction may be doubly applied (snapshot caught the write +
|
||||
// client also applies the SSE frame) — that's a renderer's problem
|
||||
// to make idempotent, not ours to avoid here.
|
||||
let seq = state.coord.current_seq();
|
||||
|
||||
let raw_containers = log_default("nixos-container list", lifecycle::list().await);
|
||||
let current_rev = crate::auto_update::current_flake_rev(&state.coord.hyperhive_flake);
|
||||
|
|
@ -720,36 +720,58 @@ fn dir_size_bytes(root: &Path) -> u64 {
|
|||
total
|
||||
}
|
||||
|
||||
async fn messages_history(State(state): State<AppState>) -> Response {
|
||||
// Backfill source for the dashboard message-flow terminal. Returns
|
||||
// up to ~200 historical broker messages as `MessageEvent::Sent` JSON
|
||||
// wrapped in `{ seq, events }`. The seq is the broker's high water
|
||||
// mark at fetch time; clients use it to dedupe their buffered live
|
||||
// SSE traffic (drop anything with `seq <= history_seq`) so a message
|
||||
async fn dashboard_history(State(state): State<AppState>) -> Response {
|
||||
// Backfill source for the dashboard terminal. Returns up to ~200
|
||||
// historical broker messages (no other event kinds are persisted)
|
||||
// converted to `DashboardEvent::Sent` JSON so the client can replay
|
||||
// through the same dispatch path as live frames. Wrapped in
|
||||
// `{ seq, events }`: the seq is the dashboard channel's high-water
|
||||
// mark at fetch time. Clients use it to dedupe their buffered live
|
||||
// SSE traffic (drop anything with `seq <= history_seq`) so a frame
|
||||
// that lands between SSE-subscribe and history-fetch isn't shown
|
||||
// twice and isn't lost.
|
||||
// twice and isn't lost. Historical rows carry `seq = 0`; the
|
||||
// boundary seq is what closes the dedupe window.
|
||||
const HISTORY_LIMIT: u64 = 200;
|
||||
// Capture seq *before* the query so the dedupe contract is
|
||||
// "drop buffered events you've already seen in history" — never
|
||||
// "lose an event that fired between the read and the timestamp."
|
||||
let seq = state.coord.broker.current_seq();
|
||||
let seq = state.coord.current_seq();
|
||||
match state.coord.broker.recent_all(HISTORY_LIMIT) {
|
||||
Ok(mut events) => {
|
||||
// recent_all returns newest-first; reverse so the replay
|
||||
// builds chronologically (matches the agent /events/history).
|
||||
events.reverse();
|
||||
Ok(mut messages) => {
|
||||
messages.reverse();
|
||||
let events: Vec<crate::dashboard_events::DashboardEvent> = messages
|
||||
.into_iter()
|
||||
.map(|m| match m {
|
||||
crate::broker::MessageEvent::Sent { from, to, body, at } => {
|
||||
crate::dashboard_events::DashboardEvent::Sent {
|
||||
seq: 0,
|
||||
from,
|
||||
to,
|
||||
body,
|
||||
at,
|
||||
}
|
||||
}
|
||||
crate::broker::MessageEvent::Delivered { from, to, body, at } => {
|
||||
crate::dashboard_events::DashboardEvent::Delivered {
|
||||
seq: 0,
|
||||
from,
|
||||
to,
|
||||
body,
|
||||
at,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
axum::Json(serde_json::json!({ "seq": seq, "events": events })).into_response()
|
||||
}
|
||||
Err(e) => error_response(&format!("messages/history failed: {e:#}")),
|
||||
Err(e) => error_response(&format!("dashboard/history failed: {e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn messages_stream(
|
||||
async fn dashboard_stream(
|
||||
State(state): State<AppState>,
|
||||
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
|
||||
let rx = state.coord.broker.subscribe();
|
||||
let rx = state.coord.dashboard_subscribe();
|
||||
let stream = BroadcastStream::new(rx).filter_map(|res| {
|
||||
// Drop lagged events. Browsers reconnect; nothing to do here.
|
||||
// Drop lagged frames. Browsers reconnect; the seq dedupe on
|
||||
// reconnect skips any frame already reflected in the snapshot.
|
||||
let event = res.ok()?;
|
||||
let json = serde_json::to_string(&event).ok()?;
|
||||
Some(Ok(Event::default().data(json)))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue