82 lines
2.4 KiB
Rust
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,
|
|
},
|
|
}))
|
|
}
|