swarm-controller: split issue-report handlers into their own module

This commit is contained in:
damocles 2026-08-31 18:22:30 +02:00 committed by mara
commit 957291e3a7
2 changed files with 124 additions and 104 deletions

View file

@ -43,6 +43,7 @@ use utoipa_axum::{router::OpenApiRouter, routes};
mod auth;
mod config_pr;
mod forge;
mod issue_report;
mod otel_http_client;
mod status;
mod vcs_metrics;
@ -1080,107 +1081,6 @@ async fn get_config_prs(
Ok(Json(cache.snapshot()))
}
/// Repos with at least one open issue, as `owner/name` full names — the
/// data source for swarm-ui's repo-filter dropdown (mara: "repo filter
/// should be dropdown (list only repos with open issues)"). Not scoped to
/// [`forge::CONFIG_ORG`] the way [`get_config_prs`] is: mara's own framing
/// for this issue-report page was "repos cannot be fixed for a hyperhive
/// feature", so this deliberately covers the whole forge instance, not
/// one org.
#[utoipa::path(
get,
path = "/api/repos",
responses(
(status = 200, description = "repos with at least one open issue, as owner/name", body = Vec<String>),
(status = 503, description = "no forge is configured on this host", body = String),
),
tag = "repos"
)]
async fn get_repos(State(state): State<AppState>) -> Result<Json<Vec<String>>, 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 (mara: "by default, i want it to not filter by
/// repo"). 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<forge::IssueReportRow>),
(status = 503, description = "no forge is configured on this host, or the report could not be built", body = String),
),
tag = "repos"
)]
async fn get_issue_report_all(
State(state): State<AppState>,
) -> Result<Json<Vec<forge::IssueReportRow>>, 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<forge::IssueReportRow>),
(status = 503, description = "no forge is configured on this host, or the report could not be built", body = String),
),
tag = "repos"
)]
async fn get_issue_report(
State(state): State<AppState>,
Path((org, repo)): Path<(String, String)>,
) -> Result<Json<Vec<forge::IssueReportRow>>, 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))
}
}
}
/// The swarm's own public base URL, as the forge must address it.
///
/// Set by `swarm-controller.nix` **only when this host actually serves the
@ -1449,9 +1349,9 @@ async fn main() -> Result<()> {
.routes(routes!(get_config_prs))
.routes(routes!(create_agent))
.routes(routes!(get_agents))
.routes(routes!(get_repos))
.routes(routes!(get_issue_report_all))
.routes(routes!(get_issue_report))
.routes(routes!(issue_report::get_repos))
.routes(routes!(issue_report::get_issue_report_all))
.routes(routes!(issue_report::get_issue_report))
.routes(routes!(webhook::post_webhook_forge))
.split_for_parts();
// Just the JSON, not the UI — Swagger UI itself is nginx-hosted from