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

@@ -1018,3 +1018,46 @@ export async function listAuditLog(
const qs = params.toString();
return request<AuditPage>(`/v1/admin/audit${qs ? `?${qs}` : ''}`, init);
}
// ---- operational health checks ---------------------------------------------
export type CheckStatus = 'ok' | 'warn' | 'critical';
export type HealthCheck = {
id: string;
label: string;
status: CheckStatus;
value: string;
threshold: string | null;
hint: string | null;
/** Admin route that remediates this check, when one applies. */
action_href: string | null;
};
export type HealthThresholds = {
disk_pct: number;
mem_pct: number;
dead_jobs_max: number;
crawl_fail_pct: number;
analysis_fail_pct: number;
cron_freshness_hours: number;
missing_covers_max: number;
};
export type HealthReport = {
status: CheckStatus;
checks: HealthCheck[];
thresholds: HealthThresholds;
};
export async function getHealth(init?: RequestInit): Promise<HealthReport> {
return request<HealthReport>('/v1/admin/health', init);
}
export async function updateHealthThresholds(t: HealthThresholds): Promise<HealthReport> {
return request<HealthReport>('/v1/admin/health/thresholds', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(t)
});
}