feat(admin): observability — job history, live now-analyzing, durations & metrics (0.84.0) (#6)
Some checks failed
deploy / test-backend (push) Failing after 19m59s
deploy / test-frontend (push) Successful in 9m54s
deploy / build-and-push (push) Has been skipped
deploy / deploy (push) Has been skipped

This commit was merged in pull request #6.
This commit is contained in:
2026-06-16 12:21:13 +00:00
parent 790549636f
commit d51ab2a049
41 changed files with 3655 additions and 21 deletions

View File

@@ -0,0 +1,43 @@
//! Timing metrics for crawler operations (see `crawl_metrics` table, 0028).
//!
//! One [`OpRow`] per completed operation (manga list walk, manga detail,
//! cover, whole chapter); [`OpSummary`] is the per-type average roll-up that
//! drives the admin Metrics tab. Per-page crawl timing is derived on the
//! client from the `chapter` summary's `avg_items` (pages/chapter), not
//! stored per image. Analysis duration lives on `page_analysis`, not here.
use chrono::{DateTime, Utc};
use serde::Serialize;
use sqlx::FromRow;
use uuid::Uuid;
/// Per-operation-type average roll-up over a time window.
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct OpSummary {
/// `manga_list` | `manga_detail` | `manga_cover` | `chapter`.
pub op: String,
/// Mean duration in milliseconds (`NULL`→absent only when `n = 0`).
pub avg_ms: Option<f64>,
/// Total operations in the window.
pub n: i64,
pub ok: i64,
pub failed: i64,
/// Mean `items` (pages/chapter, mangas/list-walk); `None` when unused.
pub avg_items: Option<f64>,
}
/// One timed operation, resolved to manga/chapter labels for the recent-ops log.
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct OpRow {
pub id: Uuid,
pub op: String,
pub manga_id: Option<Uuid>,
pub manga_title: Option<String>,
pub chapter_id: Option<Uuid>,
pub chapter_number: Option<i32>,
pub outcome: String,
pub duration_ms: i64,
pub items: Option<i32>,
pub error: Option<String>,
pub finished_at: DateTime<Utc>,
}

View File

@@ -4,6 +4,7 @@ pub mod author;
pub mod bookmark;
pub mod chapter;
pub mod collection;
pub mod crawl_metrics;
pub mod genre;
pub mod manga;
pub mod page;
@@ -25,6 +26,7 @@ pub use author::{Author, AuthorRef, AuthorWithCount};
pub use bookmark::{Bookmark, BookmarkSummary};
pub use chapter::Chapter;
pub use collection::{Collection, CollectionPageItem, CollectionSummary};
pub use crawl_metrics::{OpRow, OpSummary};
pub use genre::{Genre, GenreRef};
pub use manga::{Manga, MangaCard, MangaDetail};
pub use page::Page;

View File

@@ -157,6 +157,46 @@ pub struct PageStatusItem {
pub status: String,
}
/// One row in the admin analysis-history table: a completed or failed
/// analysis pass resolved to its page/chapter/manga context. Sourced from
/// the persistent `page_analysis` table (not the reaped job queue), so it
/// survives indefinitely. `status` is `done` | `failed`.
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct AnalysisHistoryRow {
pub page_id: Uuid,
pub page_number: i32,
pub chapter_id: Uuid,
pub chapter_number: i32,
pub manga_id: Uuid,
pub manga_title: String,
pub status: String,
pub is_nsfw: bool,
pub model: Option<String>,
pub error: Option<String>,
pub analyzed_at: Option<DateTime<Utc>>,
/// Wall-clock the worker spent on this page; `None` for rows analyzed
/// before duration tracking landed.
pub duration_ms: Option<i64>,
}
/// Per-model average analysis duration (admin Metrics tab).
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct ModelDuration {
pub model: Option<String>,
pub avg_ms: Option<f64>,
pub n: i64,
}
/// Aggregate analysis timing/outcome roll-up over a time window.
#[derive(Debug, Clone, Serialize)]
pub struct AnalysisMetrics {
pub n: i64,
pub ok: i64,
pub failed: i64,
pub avg_ms: Option<f64>,
pub by_model: Vec<ModelDuration>,
}
/// One OCR line in the page-detail view.
#[derive(Debug, Clone, Serialize, FromRow)]
pub struct OcrLine {