From c7729b2ef64a47d377a8b20dd72fff79a9d67d0d Mon Sep 17 00:00:00 2001 From: dtoro Date: Wed, 29 Jul 2026 13:37:08 +0200 Subject: [PATCH] fix(scheduler): stop monitoring deprecated/destroyed targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/db/queries/operations.sql | 2 ++ internal/db/sqlcgen/operations.sql.go | 2 ++ internal/scheduler/coverage.go | 11 ++++++++++- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/internal/db/queries/operations.sql b/internal/db/queries/operations.sql index 0742d55..03a261a 100644 --- a/internal/db/queries/operations.sql +++ b/internal/db/queries/operations.sql @@ -63,7 +63,9 @@ SELECT cd.entity_id, cd.target_id, cd.target_type, cd.kind, cd.config, e.slug AS entity_slug FROM check_defs cd JOIN entities e ON e.id = cd.entity_id +LEFT JOIN entities tgt ON tgt.id = cd.target_id WHERE cd.enabled = true + AND (tgt.id IS NULL OR tgt.state NOT IN ('deprecated', 'destroyed')) AND (cd.last_run_at IS NULL OR cd.last_run_at <= now() - make_interval(secs => cd.interval_s)); diff --git a/internal/db/sqlcgen/operations.sql.go b/internal/db/sqlcgen/operations.sql.go index 4424565..637d7b2 100644 --- a/internal/db/sqlcgen/operations.sql.go +++ b/internal/db/sqlcgen/operations.sql.go @@ -696,7 +696,9 @@ SELECT cd.entity_id, cd.target_id, cd.target_type, cd.kind, cd.config, e.slug AS entity_slug FROM check_defs cd JOIN entities e ON e.id = cd.entity_id +LEFT JOIN entities tgt ON tgt.id = cd.target_id WHERE cd.enabled = true + AND (tgt.id IS NULL OR tgt.state NOT IN ('deprecated', 'destroyed')) AND (cd.last_run_at IS NULL OR cd.last_run_at <= now() - make_interval(secs => cd.interval_s)) ` diff --git a/internal/scheduler/coverage.go b/internal/scheduler/coverage.go index ae07051..be53fbf 100644 --- a/internal/scheduler/coverage.go +++ b/internal/scheduler/coverage.go @@ -85,7 +85,16 @@ func coverageSweep(ctx context.Context, pool *db.Pool) { case !mon.Declared: undeclared++ 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: if resolveCoverageSignal(ctx, pool, e.id) { resolved++