feat(admin): overview dashboard sections + system temperature/load sensors

Rework the admin Overview tab into a full-width vertical list of
per-section summary cards (System, Crawler, Analysis, Mangas, Users,
Settings), each linking to its tab. Crawler and Analysis cards update
live over the existing SSE streams; System/Mangas/Users poll; the
Settings strip reflects daemon state. The disk/memory/CPU boxes move
into the System card.

Add a composed GET /admin/overview endpoint returning Users, Mangas and
Analysis aggregates (user counts + newest, manga sync-state counts +
chapter/page totals + newest, library analysis coverage), reusing the
existing MANGA_SYNC_STATE_CASE and the storage handler's try_join fan-out.

Extend the System tab with hardware sensors: CPU load average and
per-core usage (sysinfo), plus temperatures via sysinfo's Components API
(enabled the `component` feature). Sensors degrade to an "unavailable"
state when not exposed (e.g. inside containers without /sys access), and
a temperature at/above its critical threshold raises a warning alert.

Tests: integration tests for /admin/overview and the extended /admin/system
shape; vitest coverage for the new client types and getOverviewStats.
Bump to 0.85.0 (minor) in lockstep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-16 19:04:00 +02:00
parent d85fba7056
commit 937b6a9588
15 changed files with 1169 additions and 59 deletions

View File

@@ -86,6 +86,34 @@ async fn returns_disk_memory_cpu_alerts_shape(pool: PgPool) {
"cpu out of range: {cpu_pct}"
);
// Load average: three numeric keys. Values can legitimately be 0 on
// an idle box, so only assert presence + non-negativity.
let load = cpu["load_avg"].as_object().expect("cpu.load_avg object");
for k in ["one", "five", "fifteen"] {
let v = load[k].as_f64().unwrap_or_else(|| panic!("load_avg.{k} numeric"));
assert!(v >= 0.0, "load_avg.{k} negative: {v}");
}
// Per-core usage: an array of percentages in [0,100].
let cores = cpu["per_core"].as_array().expect("cpu.per_core array");
assert!(!cores.is_empty(), "expected at least one core");
for c in cores {
let p = c.as_f64().unwrap();
assert!((0.0..=100.0).contains(&p), "per_core out of range: {p}");
}
// Temperatures: an array (possibly empty in CI / containers without
// /sys sensors). Each present entry has a string label + numeric °C.
let temps = body
.get("temperatures")
.expect("temperatures key")
.as_array()
.expect("temperatures is an array");
for t in temps {
assert!(t["label"].is_string(), "temp label is string");
assert!(t["celsius"].as_f64().is_some(), "temp celsius is numeric");
}
let alerts = body.get("alerts").expect("alerts key").as_array().unwrap();
// Don't assert on length — the box may genuinely be >90% on memory
// when the test runs. Just confirm shape of any present entry.