/** * Outage detection for the ride detail page's Today chart. * * An outage is a contiguous run of samples with `isOpen === false`. We number * them chronologically (1-based) and report their duration so the chart can * render `` bands with hover tooltips like "Outage #4 — 1h 28m". * * Pure module — no DOM, no Recharts — so it's cheap to unit test. */ export interface OutageSample { recordedAt: string; // ISO 8601 UTC isOpen: boolean; } export interface Outage { n: number; startISO: string; endISO: string; /** Formatted HH:MM in the viewer's local timezone — matches the chart X-axis labels. */ startTimeLabel: string; endTimeLabel: string; durationMin: number; } const TIME_FMT = new Intl.DateTimeFormat([], { hour: "2-digit", minute: "2-digit", hour12: false, }); /** * Walk samples in chronological order and produce one Outage per contiguous * run of `!isOpen`. If the day ends mid-outage, the last sample becomes the * outage's `endISO` (still in progress). */ export function computeOutages(samples: OutageSample[]): Outage[] { const outages: Outage[] = []; let i = 0; let n = 0; while (i < samples.length) { if (samples[i].isOpen) { i++; continue; } const startSample = samples[i]; while (i < samples.length && !samples[i].isOpen) i++; // i now points at the first open sample after the run, or samples.length. const endSample = i < samples.length ? samples[i] : samples[samples.length - 1]; const startMs = Date.parse(startSample.recordedAt); const endMs = Date.parse(endSample.recordedAt); const durationMin = Math.max(0, Math.round((endMs - startMs) / 60_000)); n += 1; outages.push({ n, startISO: startSample.recordedAt, endISO: endSample.recordedAt, startTimeLabel: TIME_FMT.format(new Date(startSample.recordedAt)), endTimeLabel: TIME_FMT.format(new Date(endSample.recordedAt)), durationMin, }); } return outages; } /** * Format a minute count as "47m" / "1h 28m" / "2h". */ export function formatOutageDuration(min: number): string { if (min < 60) return `${min}m`; const h = Math.floor(min / 60); const m = min % 60; return m === 0 ? `${h}h` : `${h}h ${m}m`; } /** * Build a lookup from time-label (HH:MM) → outage for each closed sample. * The chart uses this to attach outage metadata to each data point so the * custom tooltip can render outage-aware text without re-scanning. */ export function outageLookup( samples: OutageSample[], outages: Outage[], ): Map { const byTime = new Map(); let oIdx = 0; let runStart: number | null = null; for (let i = 0; i < samples.length; i++) { const s = samples[i]; if (!s.isOpen) { if (runStart === null) runStart = i; const outage = outages[oIdx]; if (outage) byTime.set(TIME_FMT.format(new Date(s.recordedAt)), outage); } else if (runStart !== null) { runStart = null; oIdx += 1; } } return byTime; }