From d6adfd4cd78e2a625ed80726e54c2ecd46545dac Mon Sep 17 00:00:00 2001 From: Florent Tapponnier Date: Thu, 16 Jul 2026 15:24:33 +0200 Subject: [PATCH] downtime pill: 'DATA MISSING' wording + skip leading gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes based on 30D view review: - Leading gaps (Prom retention shorter than the visible window, harness started mid-range, provider added recently) rendered as a chart-wide band claiming every provider was down for the missing period. Skip runs that started before the first observed sample. - Rename 'DOWN' to 'DATA MISSING'. A silent series can be the provider going down, our harness losing its WebSocket, or a Prom scrape failure — the pill stays neutral instead of blaming the provider. --- src/components/time-series-chart/series.tsx | 26 +++++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/time-series-chart/series.tsx b/src/components/time-series-chart/series.tsx index fba23cf7..ca2ffa77 100644 --- a/src/components/time-series-chart/series.tsx +++ b/src/components/time-series-chart/series.tsx @@ -46,9 +46,20 @@ export function DowntimeBands({ drawn, padT, innerH }: DowntimeBandsProps) { const all: Band[] = []; for (const d of drawn) { if (d.excluded) continue; + // Only count gap runs that start AFTER the first observed sample. + // A leading run of nulls (Prom retention shorter than the visible + // window, harness started mid-range, or provider added recently) + // is "we didn't measure yet", not "provider was down for a week". + // Without this guard the 30D view on aggregator-head-lag rendered + // a chart-wide band during Prom's initial fill period. + let seenData = false; let runStart: number | null = null; const push = (endX: number) => { if (runStart == null) return; + if (!seenData) { + runStart = null; + return; + } const startX = d.pts[runStart].x; all.push({ slug: d.slug, @@ -66,9 +77,11 @@ export function DowntimeBands({ drawn, padT, innerH }: DowntimeBandsProps) { if (runStart == null) runStart = i; } else { push(d.pts[i].x); + seenData = true; } } - // Trailing gap that runs to the current time. + // Trailing gap that runs to the current time — meaningful only + // when the series had prior data (same seenData guard). if (runStart != null) push(d.pts[d.pts.length - 1].x); } if (all.length === 0) return null; @@ -133,10 +146,13 @@ export function DowntimeBands({ drawn, padT, innerH }: DowntimeBandsProps) { ))} {all.map((b, i) => { const cx = b.x + b.w / 2; - // "NAME DOWN" width estimate: ~5.5 px per char plus 14 px of - // horizontal padding (7 each side). - const label = `${b.name.toUpperCase()} DOWN`; - const pillW = Math.max(50, label.length * 5.5 + 14); + // "NAME DATA MISSING" width estimate: ~5.5 px per char plus + // 14 px of horizontal padding (7 each side). "DATA MISSING" + // stays neutral: absence of samples could be the provider + // going down, our harness losing its WebSocket, or a Prom + // scrape failure — the pill doesn't blame either side. + const label = `${b.name.toUpperCase()} DATA MISSING`; + const pillW = Math.max(80, label.length * 5.5 + 14); const pillY = padT + 2 + b.slot * (PILL_H + PILL_GAP); const pillX = Math.max(0, cx - pillW / 2); return (