use axum::{extract::State, http::StatusCode, Json};
use serde::{Deserialize, Serialize};
use super::fleet::{collect_issue_repos, collect_repo_statuses, workspace_root};
use super::sessions::ErrorResponse;
use crate::web::state::WebState;
#[derive(Serialize)]
pub struct OverviewStats {
pub session_count: i64,
pub message_count: i64,
pub project_count: i64,
pub fleet_repo_count: i64,
pub open_issue_count: i64,
pub stats_updated_at: i64,
}
#[derive(Deserialize)]
pub struct RefreshRequest {
#[serde(default)]
pub keys: Vec<String>,
}
fn internal_error(msg: impl std::fmt::Display) -> (StatusCode, Json<ErrorResponse>) {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: msg.to_string(),
}),
)
}
fn stats_from_map(map: &std::collections::HashMap<String, i64>) -> OverviewStats {
OverviewStats {
session_count: map.get("session_count").copied().unwrap_or(0),
message_count: map.get("message_count").copied().unwrap_or(0),
project_count: map.get("project_count").copied().unwrap_or(0),
fleet_repo_count: map.get("fleet_repo_count").copied().unwrap_or(0),
open_issue_count: map.get("open_issue_count").copied().unwrap_or(0),
stats_updated_at: map.get("stats_updated_at").copied().unwrap_or(0),
}
}
pub async fn get_overview_stats(
State(state): State<WebState>,
) -> Result<Json<OverviewStats>, (StatusCode, Json<ErrorResponse>)> {
let map = state
.db
.get_overview_stats()
.await
.map_err(|e| internal_error(&e))?;
Ok(Json(stats_from_map(&map)))
}
pub async fn refresh_overview_stats(
State(state): State<WebState>,
Json(body): Json<RefreshRequest>,
) -> Result<Json<OverviewStats>, (StatusCode, Json<ErrorResponse>)> {
let refresh_all = body.keys.is_empty();
let keys = &body.keys;
if refresh_all || keys.iter().any(|k| k == "fleet") {
let count = match workspace_root() {
Ok(root) => match collect_repo_statuses(&root) {
Ok(repos) => repos.len() as i64,
Err(e) => return Err(internal_error(e)),
},
Err(e) => return Err(e),
};
state
.db
.set_stat("fleet_repo_count", count)
.await
.map_err(|e| internal_error(&e))?;
}
if refresh_all || keys.iter().any(|k| k == "issues") {
let count = match compute_open_issue_count(&state).await {
Ok(n) => n,
Err(_) => {
0
}
};
state
.db
.set_stat("open_issue_count", count)
.await
.map_err(|e| internal_error(&e))?;
}
if refresh_all || keys.iter().any(|k| k == "sessions") {
let stats = state
.db
.get_stats(false)
.await
.map_err(|e| internal_error(&e))?;
state
.db
.set_stat("project_count", stats.project_count)
.await
.map_err(|e| internal_error(&e))?;
}
let now = chrono::Utc::now().timestamp();
state
.db
.set_stat("stats_updated_at", now)
.await
.map_err(|e| internal_error(&e))?;
let map = state
.db
.get_overview_stats()
.await
.map_err(|e| internal_error(&e))?;
Ok(Json(stats_from_map(&map)))
}
async fn compute_open_issue_count(state: &WebState) -> Result<i64, anyhow::Error> {
let github = state.github.read().await;
let client = github
.as_ref()
.ok_or_else(|| anyhow::anyhow!("GitHub token not configured"))?
.clone();
drop(github);
let repo_root = std::env::current_dir()?;
let repos = collect_issue_repos(&repo_root).map_err(|e| anyhow::anyhow!("{}", e))?;
let mut total: i64 = 0;
for repo in &repos {
match client
.list_issues(&repo.github.owner, &repo.github.repo, "open")
.await
{
Ok(issues) => total += issues.len() as i64,
Err(_) => {} }
}
Ok(total)
}