feat(admin): operational health checks page with editable thresholds

New /admin/health rolls up signals already collected — disk/memory sensors,
dead-job + missing-cover backlogs, 24h crawl/analysis failure rates, metadata
cron freshness, and the live crawler session/browser flags — into a flat
checks list (ok/warn/critical) with an overall status and remediation links.
The overview gains a compact Health summary banner linking in.

GET /v1/admin/health evaluates the checks (DB sub-queries run concurrently via
try_join; all hit existing indexes). PUT /v1/admin/health/thresholds persists
the thresholds to app_settings (merged over compiled defaults, forward-compat
via serde(default)), validates ranges (400 on bad input), and audit-logs the
change in one transaction. New repo::crawler::last_metadata_tick_at getter and
a lightweight system::memory_percent_used (no CPU settle delay).

Pure status helpers (over_limit for gauges, exceeds for backlog counts so a
0-limit doesn't false-alarm at 0) are unit-tested; endpoint tests cover shape,
gating, persistence+audit, and validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-19 11:38:18 +02:00
parent 31013cc893
commit 314fc8738b
12 changed files with 1265 additions and 2 deletions

View File

@@ -619,6 +619,25 @@ pub async fn last_run_completed_cleanly(
.unwrap_or(true))
}
/// Timestamp of the most recent metadata-pass cron tick, or `None` if the
/// daemon has never ticked (fresh DB / cron disabled). Stored by the daemon
/// under `crawler_state['last_metadata_tick_at']` as `{"at": <rfc3339>}`.
/// Used by the health check that flags a stale/stuck cron.
pub async fn last_metadata_tick_at(
pool: &PgPool,
) -> sqlx::Result<Option<DateTime<Utc>>> {
let row: Option<serde_json::Value> =
sqlx::query_scalar("SELECT value FROM crawler_state WHERE key = 'last_metadata_tick_at'")
.fetch_optional(pool)
.await?;
Ok(row.and_then(|v| {
v.get("at")
.and_then(|s| s.as_str())
.and_then(|s| DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&Utc))
}))
}
// ---------------------------------------------------------------------------
// Reconcile: find source-list mangas missing from the DB.
// ---------------------------------------------------------------------------