swarm-ui: add agent start/stop, backed by the wanted-state route
hyperhive#3896. The backend for start/stop (Up/Offline wanted-state declarations) already existed and was merged (#3905's writer, the PUT /api/hives/{hive}/agents/{agent}/state route) — nothing here was waiting on Paused/Destroyed, which I'd mistakenly conflated with this issue in an earlier comment (that's #3803, a different feature). swarm-controller: merges each row's declared wanted state into GET /api/agents/status, same shape as the config_pr merge (one read per distinct hive, not per agent, since a declaration is a hive's whole agent map). swarm-ui: AgentsPage gets a "wanted" column — clicking the current- state badge toggles it (Badge's own chip-plus-control shape, same as its own header comment's pause/resume example), backed by the PUT route above. A row with no declaration yet reads its implied current state off the agent's own last-reported running flag. Stop asks for confirmation (native window.confirm — no confirm-dialog component exists in swarm-ui yet); start doesn't.
This commit is contained in:
parent
d27cf6ce3e
commit
c9d8628794
3 changed files with 278 additions and 89 deletions
|
|
@ -56,6 +56,15 @@ pub struct AgentStatusRow {
|
|||
/// here, is what makes this the single call swarm-ui's agent roster
|
||||
/// page needs.
|
||||
pub config_pr: Option<crate::forge::ConfigPrStatus>,
|
||||
/// The agent's declared wanted state (`"up"`/`"offline"`), if this
|
||||
/// hive has one on record. Same rule as `config_pr`: always `None`
|
||||
/// out of this reader, filled in by the `GET /api/agents/status`
|
||||
/// handler from `AppState::wanted` — this module has no notion of a
|
||||
/// *declaration* (a swarm-level intent), only of what an agent last
|
||||
/// *reported about itself*, and merging a second bucket's read in
|
||||
/// here would blur that boundary for the same reason `config_pr`
|
||||
/// doesn't merge forge state in directly.
|
||||
pub wanted: Option<String>,
|
||||
}
|
||||
|
||||
/// A snapshot as retained by the bucket, with the hive it was published
|
||||
|
|
@ -176,6 +185,7 @@ fn render(
|
|||
age_seconds: None,
|
||||
snapshot: None,
|
||||
config_pr: None,
|
||||
wanted: None,
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -219,6 +229,7 @@ fn row(
|
|||
age_seconds: age.map(|age| age.as_secs()),
|
||||
snapshot: offered.payload.clone(),
|
||||
config_pr: None,
|
||||
wanted: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -889,22 +889,25 @@ async fn get_hives_status(
|
|||
}
|
||||
}
|
||||
|
||||
/// What each agent last said about itself, plus its open config PR if any,
|
||||
/// read at request time — the single call swarm-ui's agent roster page
|
||||
/// fills its whole table from (mara, 2026-09-02: "the view should be
|
||||
/// filled by a single backend call").
|
||||
/// What each agent last said about itself, its open config PR if any, and
|
||||
/// its declared wanted state if any, read at request time — the single
|
||||
/// call swarm-ui's agent roster page fills its whole table from (mara,
|
||||
/// 2026-09-02: "the view should be filled by a single backend call").
|
||||
///
|
||||
/// Every agent in the roster gets a row whether or not it has ever
|
||||
/// reported — see the `agent_status` module for why, and for why its hive
|
||||
/// comes from its own bucket key rather than a second lookup. `config_pr`
|
||||
/// is merged in here rather than inside `agent_status::AgentStatusReader`:
|
||||
/// that module has no forge client and is not the place to add one just
|
||||
/// for this one field, whereas this handler already holds both a
|
||||
/// roster-shaped status view and `AppState::config_prs` — the one spot
|
||||
/// that has both without a second, avoidable dependency. Absent on every
|
||||
/// row when no forge is configured here, same "absence is the answer"
|
||||
/// shape `get_config_prs` uses — not a reason to fail the whole response
|
||||
/// over a field that already knows how to be missing.
|
||||
/// and `wanted` are both merged in here rather than inside
|
||||
/// `agent_status::AgentStatusReader`: that module has no forge client or
|
||||
/// wanted-state writer and is not the place to add one just for a single
|
||||
/// field, whereas this handler already holds `AppState::config_prs` and
|
||||
/// `AppState::wanted` alongside the roster-shaped status view — the one
|
||||
/// spot that has all three without adding an avoidable dependency to a
|
||||
/// reader that stays deliberately narrow. Both fields are absent on rows
|
||||
/// where the underlying store isn't configured or has nothing on record,
|
||||
/// same "absence is the answer" shape `get_config_prs` uses — not a
|
||||
/// reason to fail the whole response over a field that already knows how
|
||||
/// to be missing.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/agents/status",
|
||||
|
|
@ -940,6 +943,47 @@ async fn get_agents_status(
|
|||
row.config_pr = config_prs.remove(&row.name);
|
||||
}
|
||||
}
|
||||
// `wanted` merges in the same way and for the same reason as
|
||||
// `config_pr` above — one read per distinct hive, not one per
|
||||
// agent, since a declaration is already a hive's whole agent
|
||||
// map.
|
||||
if let Some(writer) = state.wanted.as_ref() {
|
||||
// Owned `String` keys, not `&str` borrowed from `rows` —
|
||||
// `declarations` outlives the loop below that needs `rows`
|
||||
// mutably, so it cannot hold a live borrow into it.
|
||||
let hives: std::collections::BTreeSet<String> =
|
||||
rows.iter().filter_map(|r| r.hive.clone()).collect();
|
||||
let mut declarations: std::collections::HashMap<
|
||||
String,
|
||||
swarm_queue_client::wanted::HiveWanted,
|
||||
> = std::collections::HashMap::new();
|
||||
for hive in hives {
|
||||
match writer.view(&hive).await {
|
||||
Ok(Some(declaration)) => {
|
||||
declarations.insert(hive, declaration);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
// Missing wanted state for one hive shouldn't
|
||||
// fail the whole roster — same "absence is
|
||||
// survivable" rule `config_pr` follows above.
|
||||
tracing::warn!(
|
||||
hive, error = %format!("{e:#}"),
|
||||
"reading the wanted declaration failed; its agents show no wanted state"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for row in &mut rows {
|
||||
row.wanted = row.hive.as_deref().and_then(|hive| {
|
||||
declarations
|
||||
.get(hive)?
|
||||
.agents
|
||||
.get(&row.name)
|
||||
.map(|w| w.state.as_str().to_owned())
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(Json(rows))
|
||||
}
|
||||
Err(e) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue