Files
Mangalord/backend/src/api/admin/overview.rs
fabi cde4aca98b
All checks were successful
deploy / test-backend (push) Successful in 25m9s
deploy / test-frontend (push) Successful in 10m13s
deploy / build-and-push (push) Successful in 10m25s
deploy / deploy (push) Successful in 12s
feat(admin): overview dashboard sections + system temperature/load sensors (0.85.0) (#11)
2026-06-16 18:46:28 +00:00

82 lines
2.4 KiB
Rust

//! 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<AppState> {
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<String>,
pub newest_created_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize)]
pub struct AnalysisOverview {
pub analyzed_pages: i64,
pub total_pages: i64,
}
async fn overview(
State(state): State<AppState>,
_admin: RequireAdmin,
) -> AppResult<Json<OverviewStats>> {
// 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,
},
}))
}