dashboard: tombstones + meta_inputs events — last /api/state refetches drop
new DashboardEvent::TombstonesChanged + MetaInputsChanged carry full snapshots (lists are tiny; snapshot beats diff for race avoidance). Coordinator-side helpers emit_tombstones_snapshot + emit_meta_inputs_snapshot fire from every mutation site: actions::destroy + post_purge_tombstone + actions::approve (spawn finalise consumes tombstone) + run_meta_update + auto_update::rebuild_agent (lock bumps). client adds derived stores + apply* handlers + drops the post-submit refetch on PURG3 (container row + tombstone row) and meta-update. after this commit /api/state is fetched exactly once per page session (cold load); every other change rides the SSE channel.
This commit is contained in:
parent
76e4034e01
commit
aed43ce4df
5 changed files with 123 additions and 24 deletions
|
|
@ -87,8 +87,12 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
|
|||
}
|
||||
// New container row appeared (or didn't, on failure
|
||||
// before nixos-container create completed) — rescan so
|
||||
// dashboards reflect the post-spawn state.
|
||||
// dashboards reflect the post-spawn state. Spawn can
|
||||
// also consume a tombstone of the same name; emit the
|
||||
// fresh list so the operator's dormant-state pane
|
||||
// updates without a refetch.
|
||||
coord_bg.rescan_containers_and_emit().await;
|
||||
crate::dashboard::emit_tombstones_snapshot(&coord_bg).await;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -360,8 +364,11 @@ pub async fn destroy(coord: &Arc<Coordinator>, name: &str, purge: bool) -> Resul
|
|||
agent: name.to_owned(),
|
||||
});
|
||||
// Container row disappeared — rescan so the dashboard fires
|
||||
// `ContainerRemoved` for the gone row.
|
||||
// `ContainerRemoved` for the gone row, then emit the
|
||||
// tombstones snapshot (gained one on destroy, lost one on
|
||||
// purge — recompute either way).
|
||||
coord.rescan_containers_and_emit().await;
|
||||
crate::dashboard::emit_tombstones_snapshot(coord).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,6 +99,8 @@ pub async fn rebuild_agent(coord: &Arc<Coordinator>, name: &str, current_rev: &s
|
|||
// shifted — rescan so dashboards drop the "needs update"
|
||||
// chip without waiting for the next /api/state poll.
|
||||
coord.rescan_containers_and_emit().await;
|
||||
// Lock bump → meta-inputs panel needs to re-render.
|
||||
crate::dashboard::emit_meta_inputs_snapshot(coord);
|
||||
}
|
||||
Err(e) => {
|
||||
coord.notify_manager(&hive_sh4re::HelperEvent::Rebuilt {
|
||||
|
|
|
|||
|
|
@ -193,15 +193,15 @@ struct PortConflict {
|
|||
agents: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TombstoneView {
|
||||
name: String,
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub(crate) struct TombstoneView {
|
||||
pub name: String,
|
||||
/// Bytes used by the state dir tree. Cheap-ish to compute; let the
|
||||
/// operator know how much they're holding onto.
|
||||
state_bytes: u64,
|
||||
pub state_bytes: u64,
|
||||
/// Mtime (unix seconds) of the state dir; rough "last seen".
|
||||
last_seen: i64,
|
||||
has_creds: bool,
|
||||
pub last_seen: i64,
|
||||
pub has_creds: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
|
@ -356,19 +356,19 @@ fn build_port_conflicts(containers: &[ContainerView]) -> Vec<PortConflict> {
|
|||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
struct MetaInputView {
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub(crate) struct MetaInputView {
|
||||
/// Input key in meta's `flake.nix` — `hyperhive`, `agent-<n>`, etc.
|
||||
name: String,
|
||||
pub name: String,
|
||||
/// Full locked sha. Not displayed verbatim; the dashboard
|
||||
/// truncates to the first 12 chars for the chip.
|
||||
rev: String,
|
||||
pub rev: String,
|
||||
/// Unix seconds — `locked.lastModified`. Drives the relative
|
||||
/// "2h ago" timestamp on each input row.
|
||||
last_modified: i64,
|
||||
pub last_modified: i64,
|
||||
/// `original.url` if available, for the tooltip / row meta text.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
url: Option<String>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
/// Walk `flake.lock`'s `nodes` graph from `root` and emit one
|
||||
|
|
@ -956,6 +956,36 @@ fn resolve_state_path(raw: &str) -> std::result::Result<std::path::PathBuf, Stri
|
|||
Ok(canonical)
|
||||
}
|
||||
|
||||
/// Snapshot the current tombstone list and emit a
|
||||
/// `TombstonesChanged` event. Call after any mutation that could
|
||||
/// add or remove a tombstone (`actions::destroy`,
|
||||
/// `post_purge_tombstone`, spawn finalisation). Cheap — the list
|
||||
/// is tiny.
|
||||
pub(crate) async fn emit_tombstones_snapshot(coord: &Arc<Coordinator>) {
|
||||
let containers = coord.containers_snapshot().await;
|
||||
let transient_snapshot = coord.transient_snapshot();
|
||||
let tombstones = build_tombstone_views(coord, &containers, &transient_snapshot);
|
||||
coord.emit_dashboard_event(
|
||||
crate::dashboard_events::DashboardEvent::TombstonesChanged {
|
||||
seq: coord.next_seq(),
|
||||
tombstones,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Snapshot meta/flake.lock's root inputs + emit
|
||||
/// `MetaInputsChanged`. Call after any mutation that bumps a lock
|
||||
/// (`run_meta_update`, `auto_update::rebuild_agent`).
|
||||
pub(crate) fn emit_meta_inputs_snapshot(coord: &Coordinator) {
|
||||
let inputs = read_meta_inputs();
|
||||
coord.emit_dashboard_event(
|
||||
crate::dashboard_events::DashboardEvent::MetaInputsChanged {
|
||||
seq: coord.next_seq(),
|
||||
inputs,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Scan `body` for path-shaped tokens, validate each against the
|
||||
/// allow-list, return the unique set of tokens that resolve to a
|
||||
/// regular file. Called at broker-message ingest time so the
|
||||
|
|
@ -1094,9 +1124,10 @@ async fn post_purge_tombstone(
|
|||
.fail_pending_for_agent(&name, "agent state purged");
|
||||
if errors.is_empty() {
|
||||
tracing::info!(%name, "tombstone purged");
|
||||
// Tombstones aren't event-derived yet, so the client still
|
||||
// refetches /api/state to see this one disappear (matching
|
||||
// form omits `data-no-refresh`).
|
||||
// Fire the post-purge tombstones snapshot so dashboards
|
||||
// drop the row live; matching form carries
|
||||
// `data-no-refresh`.
|
||||
emit_tombstones_snapshot(&state.coord).await;
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
} else {
|
||||
error_response(&format!("purge {name} partial: {}", errors.join(", ")))
|
||||
|
|
@ -1148,10 +1179,10 @@ async fn post_meta_update(
|
|||
let inputs_clone = inputs.clone();
|
||||
tokio::spawn(async move {
|
||||
run_meta_update(&coord, &inputs_clone).await;
|
||||
// Lock file changed — emit so dashboards refresh the
|
||||
// meta-inputs panel without a snapshot poll.
|
||||
emit_meta_inputs_snapshot(&coord);
|
||||
});
|
||||
// Background task — each per-agent rebuild emits its own
|
||||
// `ContainerStateChanged`; the meta inputs panel still relies on
|
||||
// /api/state freshness (matching form omits `data-no-refresh`).
|
||||
(StatusCode::OK, "ok").into_response()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use crate::container_view::ContainerView;
|
||||
use crate::dashboard::{MetaInputView, TombstoneView};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "snake_case", tag = "kind")]
|
||||
|
|
@ -156,4 +157,25 @@ pub enum DashboardEvent {
|
|||
/// `nixos-container destroy` (operator-driven or otherwise) on the
|
||||
/// next rescan.
|
||||
ContainerRemoved { seq: u64, name: String },
|
||||
/// Full snapshot of the tombstones list. Emitted on every
|
||||
/// mutation that could add / remove a tombstone: destroy
|
||||
/// (with or without purge), purge-tombstone, spawn approval
|
||||
/// (which can consume a tombstone of the same name). Snapshot
|
||||
/// shape (not diff) because the list is tiny (single-digit
|
||||
/// typical) and recomputing avoids the add/remove races a
|
||||
/// per-row event would have.
|
||||
TombstonesChanged {
|
||||
seq: u64,
|
||||
tombstones: Vec<TombstoneView>,
|
||||
},
|
||||
/// Full snapshot of `meta/flake.lock`'s root inputs. Emitted
|
||||
/// after every operation that bumps a lock: `meta-update`,
|
||||
/// `rebuild_agent` (lock bumps via two-phase staging),
|
||||
/// `update-all`. Same snapshot-shape rationale as
|
||||
/// `TombstonesChanged` — the list is small (one row per agent
|
||||
/// plus their fetched inputs).
|
||||
MetaInputsChanged {
|
||||
seq: u64,
|
||||
inputs: Vec<MetaInputView>,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue