//! Agents' open config PRs. use anyhow::Result; use axum::{ Json, extract::{Path, State}, }; use crate::forge; use crate::routes::{AppState, StatusUnavailable}; /// `agent`'s open config PR, from [`ConfigPrCache`](crate::config_pr::ConfigPrCache) — kept /// current by a webhook nudge (near-real-time) backstopped by a periodic /// scan (worst case: up to one `config_pr::POLL_INTERVAL` stale if a /// delivery was ever missed). Not a live forge read either way, so this /// answers even when the forge itself is momentarily unreachable. /// /// `200` with a `null` body means "no open PR" (or "no scan has completed /// yet") — not distinguished, for the same reason /// [`ConfigPrCache::get`](crate::config_pr::ConfigPrCache::get) doesn't: both read as "nothing to show" /// to the swarm-ui config-PR panel this feeds, and a cache is a best-effort /// read, not a source of truth callers should expect to disambiguate /// against. #[utoipa::path( get, path = "/api/agents/{name}/config-pr", params(("name" = String, Path, description = "agent name")), responses( (status = 200, description = "the agent's open config PR, or null if none / not scanned yet", body = Option), (status = 503, description = "no forge is configured on this host", body = String), ), tag = "agents" )] pub(crate) async fn get_agent_config_pr( State(state): State, Path(name): Path, ) -> Result>, StatusUnavailable> { let Some(cache) = state.config_prs.get() else { return Err(StatusUnavailable( "no forge is configured on this host".to_owned(), )); }; Ok(Json(cache.get(&name))) } /// Every agent with an open config PR, in one response — the bulk /// counterpart to [`get_agent_config_pr`]. swarm-ui's config-PR table needs /// every agent's status to render, and fetching them one at a time doesn't /// scale and isn't how the rest of swarm-ui's single-fetch pages (jobq, /// hives status) work. /// /// Deliberately **not** `/api/agents/config-prs`: agent names are /// user-specified (any string `hive_types::Ident` accepts), so a literal /// path segment sitting where a `{name}` capture could plausibly also want /// to live is a real, not theoretical, clash risk the moment someone names /// an agent `config-prs` — per mara's review call, closed off by /// construction rather than relying on axum's static-route-priority /// tiebreak to paper over it. /// /// Only agents with a currently-open PR are present — same "absence is the /// answer" shape [`get_agent_config_pr`]'s `null` uses, just at map-entry /// granularity instead of per-request. #[utoipa::path( get, path = "/api/config-prs", responses( (status = 200, description = "agent name -> open config PR, only agents with one present", body = std::collections::HashMap), (status = 503, description = "no forge is configured on this host", body = String), ), tag = "agents" )] pub(crate) async fn get_config_prs( State(state): State, ) -> Result>, StatusUnavailable> { let Some(cache) = state.config_prs.get() else { return Err(StatusUnavailable( "no forge is configured on this host".to_owned(), )); }; Ok(Json(cache.snapshot())) }