feat(api): admin manga/chapter overview with derived sync state (0.39.0)
Adds GET /api/v1/admin/mangas and /admin/mangas/:id/chapters guarded by RequireAdmin. Sync state is computed at query time from the existing crawler signals (manga_sources / chapter_sources / crawler_jobs) — no new state column is persisted, so the crawler stays the single writer of these signals. Per-manga priority: InProgress (in-flight sync_manga or sync_chapter_list job) > Dropped (all source rows soft-dropped) > Synced (default; covers user-uploaded mangas with zero source rows). Per-chapter priority: Downloading (in-flight sync_chapter_content) > Dropped (all source rows soft-dropped) > Failed (most-recent terminal job is dead) > NotDownloaded (page_count = 0) > Synced. The Failed check sits ABOVE NotDownloaded so the more informative "we tried and it died" state wins over "we never got around to it" — see the priority comment in repo/admin_view.rs. Migration 0020 adds a partial index on crawler_jobs((payload->>'source_manga_key')) for the one job kind (sync_manga) whose payload doesn't carry manga_id directly — without it the in-flight detection for a manga falls back to a seqscan over the job table.
This commit is contained in:
46
backend/src/domain/sync_state.rs
Normal file
46
backend/src/domain/sync_state.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! Sync-state enums derived per-manga / per-chapter from `manga_sources`,
|
||||
//! `chapter_sources`, and `crawler_jobs` at query time. No state column
|
||||
//! is persisted on `mangas` / `chapters` — see `repo::admin_view` for the
|
||||
//! derivation rules and priority order.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
|
||||
#[sqlx(type_name = "text", rename_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MangaSyncState {
|
||||
/// A `sync_manga` or `sync_chapter_list` job is currently
|
||||
/// pending or running for this manga.
|
||||
InProgress,
|
||||
/// At least one `manga_sources` row exists for this manga and ALL of
|
||||
/// them have `dropped_at IS NOT NULL` — every source we know about
|
||||
/// has stopped surfacing it.
|
||||
Dropped,
|
||||
/// Default healthy state: at least one live source row OR the manga
|
||||
/// was user-uploaded (no `manga_sources` rows at all).
|
||||
Synced,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, sqlx::Type)]
|
||||
#[sqlx(type_name = "text", rename_all = "snake_case")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChapterSyncState {
|
||||
/// A `sync_chapter_content` job is currently pending or running for
|
||||
/// this chapter (the 0014 dedup index guarantees at most one).
|
||||
Downloading,
|
||||
/// At least one `chapter_sources` row exists AND all of them are
|
||||
/// `dropped_at IS NOT NULL`.
|
||||
Dropped,
|
||||
/// Most recent `sync_chapter_content` job for this chapter is `dead`
|
||||
/// (terminal failure). Checked BEFORE `NotDownloaded` so the more
|
||||
/// informative "we tried and it died" state wins over "we never
|
||||
/// got around to it".
|
||||
Failed,
|
||||
/// `page_count = 0` and no in-flight or failed job — the chapter
|
||||
/// row exists but content has never been downloaded.
|
||||
NotDownloaded,
|
||||
/// `page_count > 0` — content has been downloaded at some point.
|
||||
/// Reaped `done` jobs in `crawler_jobs` mean we can't read this from
|
||||
/// the job table, so `page_count` is the durable truth.
|
||||
Synced,
|
||||
}
|
||||
Reference in New Issue
Block a user