// Pure status math — no React, no api. Kept here so it's unit-testable on its // own (see page.test.tsx) and reused by the page's banner + uptime grid. export interface Incident { readonly id: string readonly title: string readonly impact: string // 'minor' | 'major' | 'critical' readonly status: string // 'investigating' | 'identified' | 'monitoring' | 'resolved' readonly startedAt: string | Date readonly resolvedAt: string | Date | null } export interface Component { readonly id: string readonly name: string readonly status: string // 'operational' | 'degraded' | 'partial_outage' | 'major_outage' } export interface Update { readonly id: string readonly incidentId: string readonly body: string readonly status: string readonly createdAt: string | Date } export type Overall = 'operational' | 'degraded' | 'major_outage' const isOpen = (i: Incident): boolean => i.status !== 'resolved' // The banner state, from current incidents + component statuses. A live critical // incident (or a component in major_outage) is the worst; any other open // incident or non-operational component is a partial degradation. export const deriveOverall = ( incidents: ReadonlyArray, components: ReadonlyArray, ): Overall => { const open = incidents.filter(isOpen) const worstOpen = open.some((i) => i.impact === 'critical') const worstComponent = components.some((c) => c.status === 'major_outage') if (worstOpen || worstComponent) return 'major_outage' if (open.length > 0 || components.some((c) => c.status !== 'operational')) return 'degraded' return 'operational' } const DAY_MS = 24 * 60 * 60 * 1000 const ms = (v: string | Date): number => (v instanceof Date ? v.getTime() : Date.parse(v)) /** * A 90-day uptime grid (oldest → newest). A day counts as "down" when a * major-or-critical incident overlapped it — derived from incident timestamps, * NOT external monitoring, so operators control the narrative. `now` is a seam * for deterministic tests. */ export const uptimeGrid = ( incidents: ReadonlyArray, now: number = Date.now(), days = 90, ): ReadonlyArray => { const impactful = incidents.filter((i) => i.impact === 'major' || i.impact === 'critical') const todayStart = Math.floor(now / DAY_MS) * DAY_MS const grid: boolean[] = [] for (let d = days - 1; d >= 0; d--) { const dayStart = todayStart - d * DAY_MS const dayEnd = dayStart + DAY_MS const down = impactful.some((i) => { const start = ms(i.startedAt) const end = i.resolvedAt == null ? now : ms(i.resolvedAt) return start < dayEnd && end >= dayStart }) grid.push(!down) } return grid } // Uptime percentage over the grid window, to one decimal. export const uptimePercent = (grid: ReadonlyArray): number => { if (grid.length === 0) return 100 const up = grid.filter(Boolean).length return Math.round((up / grid.length) * 1000) / 10 }