//! The swarm-ui issue-report page's data source — repo listing plus the //! two shapes of the report itself (all-repos default, single-repo //! filtered). See [`crate::forge::Client::issue_report`] and //! [`crate::forge::Client::issue_report_all`] for how a row's `blocked` / //! `depended_on_by_count` fields are actually computed; this module is //! just the HTTP surface over that logic, split out into its own file — //! same shape [`crate::webhook`] already uses: business logic in //! `crate::forge::Client`, the axum handlers next to their own routes. use axum::{ Json, extract::{Path, State}, }; use super::{AppState, StatusUnavailable}; use crate::forge; /// Repos with at least one open issue, as `owner/name` full names — the /// data source for swarm-ui's repo-filter dropdown. Not scoped to /// `forge::CONFIG_ORG` the way `main.rs`'s `get_config_prs` is: this /// report deliberately covers the whole forge instance, not one org, since /// the report itself is a general-purpose browsing tool rather than /// something agent-config-specific. #[utoipa::path( get, path = "/api/repos", responses( (status = 200, description = "repos with at least one open issue, as owner/name", body = Vec), (status = 503, description = "no forge is configured on this host", body = String), ), tag = "repos" )] pub async fn get_repos( State(state): State, ) -> Result>, StatusUnavailable> { let Some(client) = state.forge.as_ref() else { return Err(StatusUnavailable( "no forge is configured on this host".to_owned(), )); }; match client.list_repos_with_open_issues().await { Ok(repos) => Ok(Json(repos)), Err(e) => { let detail = format!("{e:#}"); tracing::warn!(error = %detail, "listing repos failed"); Err(StatusUnavailable(detail)) } } } /// Every open issue across every repo with at least one — the default, /// no-repo-filter view. See [`get_issue_report`] for the single-repo /// counterpart (used once the dropdown's repo filter is set) and /// [`forge::Client::issue_report_all`] for how the fan-out works. #[utoipa::path( get, path = "/api/issue-report", responses( (status = 200, description = "every open issue across every repo with one, blocked/dependent-count pre-resolved", body = Vec), (status = 503, description = "no forge is configured on this host, or the report could not be built", body = String), ), tag = "repos" )] pub async fn get_issue_report_all( State(state): State, ) -> Result>, StatusUnavailable> { let Some(client) = state.forge.as_ref() else { return Err(StatusUnavailable( "no forge is configured on this host".to_owned(), )); }; match client.issue_report_all().await { Ok(rows) => Ok(Json(rows)), Err(e) => { let detail = format!("{e:#}"); tracing::warn!(error = %detail, "building the all-repos issue report failed"); Err(StatusUnavailable(detail)) } } } /// Every open issue in `{org}/{repo}` specifically — the filtered view once /// swarm-ui's repo dropdown (populated by [`get_repos`]) has a selection. /// See [`forge::Client::issue_report`] for how `blocked` / /// `depended_on_by_count` are computed and why this has to be a /// server-side pass rather than something swarm-ui resolves itself per row. #[utoipa::path( get, path = "/api/repos/{org}/{repo}/issue-report", params( ("org" = String, Path, description = "repo owner (user or org)"), ("repo" = String, Path, description = "repo name"), ), responses( (status = 200, description = "every open issue in this repo, blocked/dependent-count pre-resolved", body = Vec), (status = 503, description = "no forge is configured on this host, or the report could not be built", body = String), ), tag = "repos" )] pub async fn get_issue_report( State(state): State, Path((org, repo)): Path<(String, String)>, ) -> Result>, StatusUnavailable> { let Some(client) = state.forge.as_ref() else { return Err(StatusUnavailable( "no forge is configured on this host".to_owned(), )); }; match client.issue_report(&org, &repo).await { Ok(rows) => Ok(Json(rows)), Err(e) => { let detail = format!("{e:#}"); tracing::warn!(error = %detail, %org, %repo, "building issue report failed"); Err(StatusUnavailable(detail)) } } }