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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user