swarm-controller+swarm-ui: merge config-PR into GET /api/agents/status

Per mara's review call on this PR: "the view should be filled by a single
backend call." AgentsPage.tsx was doing three fetches (/api/agents,
/api/config-prs, /api/agents/status) and joining them client-side by name.

Moves the config-PR join server-side instead: AgentStatusRow gains a
config_pr field, populated by get_agents_status's handler from
AppState::config_prs after agent_status::AgentStatusReader::view() returns
- not inside that module, which has no forge client and stays that way (see
the field's doc comment for why the handler is the right layer for this
merge, not the reader).

AgentsPage.tsx now does exactly one fetch and no client-side joining at all
- the wire row is the table row. Dropped the separate AgentStatusRow TS
interface (folded into AgentRow, which now mirrors the backend type
field-for-field) and the /api/agents + /api/config-prs fetches entirely;
neither is needed once /api/agents/status already returns every roster
agent with its config PR attached.

ConfigPrStatus gained Deserialize (previously Serialize-only) since
AgentStatusRow derives both and a struct's derive requires every field to
support it.
This commit is contained in:
iris 2026-09-02 12:42:06 +02:00 committed by mara
commit 50650476ad
4 changed files with 69 additions and 61 deletions

View file

@ -45,6 +45,17 @@ pub struct AgentStatusRow {
/// failed to decode as [`swarm_queue_client::agent_status::AgentStatus`]
/// (logged as a warning naming the agent).
pub snapshot: Option<serde_json::Value>,
/// The agent's open config PR, if any. Always `None` coming out of
/// [`AgentStatusReader::view`] itself — this module has no forge
/// client and stays that way, same separation
/// [`crate::status`]/[`crate::agent_status`] already keep from each
/// other. `GET /api/agents/status`'s handler fills this field in
/// after the fact from `AppState::config_prs`, which is the one
/// place that already holds both a roster-shaped answer and a
/// config-PR cache — see the handler for why merging there, not
/// here, is what makes this the single call swarm-ui's agent roster
/// page needs.
pub config_pr: Option<crate::forge::ConfigPrStatus>,
}
/// A snapshot as retained by the bucket, with the hive it was published
@ -164,6 +175,7 @@ fn render(
last_seen_unix: None,
age_seconds: None,
snapshot: None,
config_pr: None,
},
})
.collect();
@ -206,6 +218,7 @@ fn row(
.and_then(|d| i64::try_from(d.as_secs()).ok()),
age_seconds: age.map(|age| age.as_secs()),
snapshot: offered.payload.clone(),
config_pr: None,
}
}