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:
parent
00cca0c903
commit
50650476ad
4 changed files with 69 additions and 61 deletions
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ use forgejo_api::structs::{
|
|||
use forgejo_api::{ApiErrorKind, Auth, Forgejo, ForgejoError};
|
||||
use futures_util::{StreamExt as _, TryStreamExt as _};
|
||||
use reqwest::StatusCode;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ use crate::webhook::DeliveryKind;
|
|||
|
||||
/// An agent's open config-PR, as [`Client::list_open_config_prs`] reports it
|
||||
/// and `GET /api/agents/{name}/config-pr` serves it.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, ToSchema)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ToSchema)]
|
||||
pub struct ConfigPrStatus {
|
||||
pub pr_number: u64,
|
||||
/// Absent only if Forgejo itself omitted the field — every real PR has
|
||||
|
|
|
|||
|
|
@ -889,17 +889,27 @@ async fn get_hives_status(
|
|||
}
|
||||
}
|
||||
|
||||
/// What each agent last said about itself, read from the swarm queue at
|
||||
/// request time.
|
||||
/// 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").
|
||||
///
|
||||
/// 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.
|
||||
/// 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.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/agents/status",
|
||||
responses(
|
||||
(status = 200, description = "a row per agent, freshness derived now", body = Vec<agent_status::AgentStatusRow>),
|
||||
(status = 200, description = "a row per agent, freshness + config PR derived now", body = Vec<agent_status::AgentStatusRow>),
|
||||
(status = 503, description = "no swarm queue is configured here, no identity bridge is configured, or either store could not be read", body = String),
|
||||
),
|
||||
tag = "agents"
|
||||
|
|
@ -923,7 +933,15 @@ async fn get_agents_status(
|
|||
StatusUnavailable(detail)
|
||||
})?;
|
||||
match reader.view(&roster, std::time::SystemTime::now()).await {
|
||||
Ok(rows) => Ok(Json(rows)),
|
||||
Ok(mut rows) => {
|
||||
if let Some(cache) = state.config_prs.as_ref() {
|
||||
let mut config_prs = cache.snapshot();
|
||||
for row in &mut rows {
|
||||
row.config_pr = config_prs.remove(&row.name);
|
||||
}
|
||||
}
|
||||
Ok(Json(rows))
|
||||
}
|
||||
Err(e) => {
|
||||
let detail = format!("{e:#}");
|
||||
tracing::warn!(error = %detail, "reading the agent-status bucket failed");
|
||||
|
|
|
|||
Loading…
Reference in a new issue