fix(scheduler): stop monitoring deprecated/destroyed targets

ListEnabledCheckDefs now LEFT JOINs the target entity and excludes rows whose
target is deprecated or destroyed, so retired things (secrets-issuance,
homelab-mcp, the dead secrets ingress route) stop generating permanent false
alarms instead of waiting for an operator to disable the check_def by hand.

coverageSweep's None() branch previously did nothing, so a type changed from
declared monitoring to `monitoring: none` (dns-zone) left its open
`unmonitored` signals lingering forever — a None() entity never gains a check,
so the hasCheck resolution path never fired. It now resolves those signals.
This commit is contained in:
2026-07-29 13:37:08 +02:00
parent b8b4aa2aee
commit c7729b2ef6
3 changed files with 14 additions and 1 deletions

View File

@@ -63,7 +63,9 @@ SELECT cd.entity_id, cd.target_id, cd.target_type, cd.kind, cd.config,
e.slug AS entity_slug e.slug AS entity_slug
FROM check_defs cd FROM check_defs cd
JOIN entities e ON e.id = cd.entity_id JOIN entities e ON e.id = cd.entity_id
LEFT JOIN entities tgt ON tgt.id = cd.target_id
WHERE cd.enabled = true WHERE cd.enabled = true
AND (tgt.id IS NULL OR tgt.state NOT IN ('deprecated', 'destroyed'))
AND (cd.last_run_at IS NULL AND (cd.last_run_at IS NULL
OR cd.last_run_at <= now() - make_interval(secs => cd.interval_s)); OR cd.last_run_at <= now() - make_interval(secs => cd.interval_s));

View File

@@ -696,7 +696,9 @@ SELECT cd.entity_id, cd.target_id, cd.target_type, cd.kind, cd.config,
e.slug AS entity_slug e.slug AS entity_slug
FROM check_defs cd FROM check_defs cd
JOIN entities e ON e.id = cd.entity_id JOIN entities e ON e.id = cd.entity_id
LEFT JOIN entities tgt ON tgt.id = cd.target_id
WHERE cd.enabled = true WHERE cd.enabled = true
AND (tgt.id IS NULL OR tgt.state NOT IN ('deprecated', 'destroyed'))
AND (cd.last_run_at IS NULL AND (cd.last_run_at IS NULL
OR cd.last_run_at <= now() - make_interval(secs => cd.interval_s)) OR cd.last_run_at <= now() - make_interval(secs => cd.interval_s))
` `

View File

@@ -85,7 +85,16 @@ func coverageSweep(ctx context.Context, pool *db.Pool) {
case !mon.Declared: case !mon.Declared:
undeclared++ undeclared++
case mon.None(): case mon.None():
// Explicitly unmonitorable. Nothing to say. // Explicitly unmonitorable. Nothing to raise — but a type that
// USED to declare monitoring (e.g. dns-zone, [dns]→none) may have
// open `unmonitored` signals from before the change. They are no
// longer a gap, so close them; otherwise they linger forever,
// because resolveCoverageSignal only runs from the hasCheck path
// and a None() entity never gains a check.
if resolveCoverageSignal(ctx, pool, e.id) {
resolved++
slog.Info("scheduler: type now unmonitorable, resolving stale signal", "entity", e.slug)
}
case e.hasCheck: case e.hasCheck:
if resolveCoverageSignal(ctx, pool, e.id) { if resolveCoverageSignal(ctx, pool, e.id) {
resolved++ resolved++