use crate::prelude::*;
use axum::{extract::State, http::StatusCode, Json};
use super::fleet::{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: &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 db = state.db.clone();
let map = tokio::task::spawn_blocking(move || db.session_repo.get_overview_stats())
.await
.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
.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),
};
let db = state.db.clone();
tokio::task::spawn_blocking(move || db.session_repo.set_stat("fleet_repo_count", count))
.await
.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
.map_err(|e| internal_error(&e))?;
}
if refresh_all || keys.iter().any(|k| k == "issues") {
let count = open_issue_count(&state)?;
let db = state.db.clone();
tokio::task::spawn_blocking(move || db.session_repo.set_stat("open_issue_count", count))
.await
.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
.map_err(|e| internal_error(&e))?;
}
if refresh_all || keys.iter().any(|k| k == "sessions") {
let db = state.db.clone();
let stats = tokio::task::spawn_blocking(move || db.session_repo.get_stats(false))
.await
.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
.map_err(|e| internal_error(&e))?;
let db = state.db.clone();
tokio::task::spawn_blocking(move || {
db.session_repo
.set_stat("project_count", stats.project_count)
})
.await
.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
.map_err(|e| internal_error(&e))?;
}
let now = chrono::Utc::now().timestamp();
let db = state.db.clone();
tokio::task::spawn_blocking(move || db.session_repo.set_stat("stats_updated_at", now))
.await
.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
.map_err(|e| internal_error(&e))?;
let db = state.db.clone();
let map = tokio::task::spawn_blocking(move || db.session_repo.get_overview_stats())
.await
.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
.map_err(|e| internal_error(&e))?;
Ok(Json(stats_from_map(&map)))
}
fn open_issue_count(state: &WebState) -> Result<i64, (StatusCode, Json<ErrorResponse>)> {
let github = crate::web::github::of(state)?;
let repositories = crate::web::github::repositories(state)?;
let facts = github
.service
.overview(&repositories)
.map_err(crate::web::github::failure)?;
Ok(facts.values().map(|facts| facts.open_issues).sum())
}