diff --git a/src/lib/server/db/repositories/monitoring.ts b/src/lib/server/db/repositories/monitoring.ts index d171c1f0..3e12b3b8 100644 --- a/src/lib/server/db/repositories/monitoring.ts +++ b/src/lib/server/db/repositories/monitoring.ts @@ -333,8 +333,8 @@ export class MonitoringRepository extends BaseRepository { * Recent samples the Confirmation Threshold resolver needs, newest first: scheduled-check * observations (REALTIME/TIMEOUT/ERROR) plus incident/maintenance overlays. MANUAL pushes * and DEFAULT fill are excluded — they stay transparent to the counter. Returns `type` so - * the resolver can stop at overlay rows (freeze); observations whose status is NO_DATA are - * skipped as neutral by the resolver. + * the resolver can stop at overlay rows (freeze). Observations whose status is NO_DATA are + * excluded entirely (neutral — they neither advance nor reset the count and must not consume lookback slots). */ async getRecentSamplesForConfirmation( monitor_tag: string, @@ -346,6 +346,7 @@ export class MonitoringRepository extends BaseRepository { .where("monitor_tag", monitor_tag) .where("timestamp", "<", beforeTs) .whereIn("type", [...OBSERVED_CHECK_TYPES, ...OVERLAY_TYPES]) + .whereNot("status", GC.NO_DATA) .orderBy("timestamp", "desc") .limit(limit); } diff --git a/src/lib/server/services/confirmationThreshold.ts b/src/lib/server/services/confirmationThreshold.ts index a553b4bd..152bf9bb 100644 --- a/src/lib/server/services/confirmationThreshold.ts +++ b/src/lib/server/services/confirmationThreshold.ts @@ -32,8 +32,9 @@ export interface ConfirmationDeps { // Overlay sample types that freeze the count (must match OVERLAY_TYPES in the monitoring repository). const OVERLAY_TYPES: string[] = [GC.INCIDENT, GC.MAINTENANCE]; -// Extra lookback rows beyond the threshold, to tolerate neutral (NO_DATA) rows that are skipped -// rather than counted. Pathologically neutral-dense histories may delay confirmation by a check. +// Extra lookback rows beyond the threshold: headroom for the anchor row and any interleaved +// overlay rows. NO_DATA observations are excluded by the query (neutral), so they never +// consume slots — the buffer does not need to scale with NO_DATA density. const LOOKBACK_BUFFER = 10; /** @@ -90,7 +91,7 @@ export async function resolveConfirmedStatus( for (const row of recent) { if (row.type !== null && OVERLAY_TYPES.indexOf(row.type) !== -1) break; // freeze boundary const rawSide = sideOf(row.raw_status); - if (rawSide === null) continue; // NO_DATA: neutral — neither advance nor reset + if (rawSide === null) continue; // NO_DATA: neutral (excluded by the query; this is a defensive guard) if (rawSide === observedSide && sideOf(row.status) === confirmedSide) { pendingRun++; pendingTimestamps.push(row.timestamp); @@ -119,5 +120,6 @@ function confirmedSideStatus( if (row.type !== null && OVERLAY_TYPES.indexOf(row.type) !== -1) continue; if (sideOf(row.status) === confirmedSide && row.status) return row.status; } + // Defensive fallback (unreachable when an anchor was found above); side is correct, severity may coarsen. return confirmedSide === "UP" ? GC.UP : GC.DOWN; }