//! Admin overview dashboard aggregates. //! //! One composed endpoint feeding the Overview tab's per-section summary //! cards (Users, Mangas, Analysis). Pure DB aggregates — no system //! sampling — so it's cheap enough to poll. The System and Crawler cards //! reuse their own existing endpoints (`/admin/system`, `/admin/crawler`), //! and the Settings strip reuses the settings endpoints; only these three //! sections needed new aggregation, so they live together here. //! //! Admin-only (`RequireAdmin`, cookie-only). use axum::extract::State; use axum::routing::get; use axum::{Json, Router}; use chrono::{DateTime, Utc}; use serde::Serialize; use crate::app::AppState; use crate::auth::extractor::RequireAdmin; use crate::error::AppResult; use crate::repo; pub fn routes() -> Router { Router::new().route("/admin/overview", get(overview)) } #[derive(Debug, Serialize)] pub struct OverviewStats { pub users: UsersOverview, pub mangas: repo::admin_view::MangaStats, pub analysis: AnalysisOverview, } #[derive(Debug, Serialize)] pub struct UsersOverview { pub total: i64, pub admins: i64, pub newest_username: Option, pub newest_created_at: Option>, } #[derive(Debug, Serialize)] pub struct AnalysisOverview { pub analyzed_pages: i64, pub total_pages: i64, } async fn overview( State(state): State, _admin: RequireAdmin, ) -> AppResult> { // Independent reads — run concurrently so latency is the slowest // query, not their sum (mirrors the storage handler). let (user_counts, user_newest, mangas, coverage) = tokio::try_join!( repo::user::counts(&state.db), repo::user::newest(&state.db), repo::admin_view::manga_stats(&state.db), repo::page_analysis::library_coverage(&state.db), )?; let (total, admins) = user_counts; let (newest_username, newest_created_at) = match user_newest { Some((username, created_at)) => (Some(username), Some(created_at)), None => (None, None), }; let (analyzed_pages, total_pages) = coverage; Ok(Json(OverviewStats { users: UsersOverview { total, admins, newest_username, newest_created_at, }, mangas, analysis: AnalysisOverview { analyzed_pages, total_pages, }, })) }