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:
iris 2026-09-02 19:41:37 +02:00
commit c9d8628794
3 changed files with 278 additions and 89 deletions

View file

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