"use client"; import { AlertTriangle, CheckCircle2, ChevronDown, Wrench, XCircle } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { type KeyboardEvent, type ReactNode, useId, useMemo, useRef, useState } from "react"; import { Badge, type BadgeVariant } from "@/components/motion/badge"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/motion/collapsible"; import { EASE_OUT, SPRING_SWAP } from "@/lib/ease"; import { cn } from "@/lib/utils"; export type ServiceDayStatus = | "operational" | "degraded" | "partial" | "major" | "maintenance" | "nodata"; export interface ServiceDay { /** Calendar day, `YYYY-MM-DD` (read as UTC so server and client agree). */ date: string; status: ServiceDayStatus; /** Incident titles that touched this day; shown in the bar tooltip. */ incidents?: string[]; } export interface StatusService { id: string; name: string; description?: string; /** Oldest first; typically 90 entries. The last day is "today". */ days: ServiceDay[]; /** Override the computed uptime percentage. */ uptime?: number; } export type IncidentStatus = "investigating" | "identified" | "monitoring" | "resolved"; export type IncidentImpact = "minor" | "major" | "critical" | "maintenance"; export interface IncidentUpdate { id: string; status: IncidentStatus; message: string; /** ISO timestamp. */ at: string; } export interface StatusIncident { id: string; title: string; impact: IncidentImpact; status: IncidentStatus; /** Newest first. */ updates: IncidentUpdate[]; /** Affected service names. */ services?: string[]; } export interface StatusPageProps { services: StatusService[]; incidents?: StatusIncident[]; title?: ReactNode; /** Pre-formatted "last updated" text (never computed, so markup stays deterministic). */ lastUpdated?: ReactNode; /** Ids of incidents expanded on mount. Default: unresolved ones. */ defaultExpandedIncidents?: string[]; locale?: string; /** IANA time zone for incident timestamps. Default "UTC" so SSR matches. */ timeZone?: string; className?: string; } const DAY_TONE: Record = { operational: "bg-success", degraded: "bg-warning/70", partial: "bg-warning", major: "bg-destructive", maintenance: "bg-chart-2", nodata: "bg-foreground/10", }; const DAY_LABEL: Record = { operational: "No downtime", degraded: "Degraded performance", partial: "Partial outage", major: "Major outage", maintenance: "Scheduled maintenance", nodata: "No data", }; // Share of the day counted as up, for the computed uptime figure. const DAY_UPTIME: Record = { operational: 1, degraded: 0.998, partial: 0.99, major: 0.95, maintenance: 1, nodata: null, }; const SEVERITY: ServiceDayStatus[] = ["operational", "maintenance", "degraded", "partial", "major"]; const IMPACT_BADGE: Record = { minor: "warning", major: "destructive", critical: "destructive", maintenance: "secondary", }; const STATUS_TEXT: Record = { investigating: "Investigating", identified: "Identified", monitoring: "Monitoring", resolved: "Resolved", }; export function computeUptime(days: ServiceDay[]) { const counted = days .map((d) => DAY_UPTIME[d.status]) .filter((v): v is number => v != null); if (counted.length === 0) return 100; return (counted.reduce((a, b) => a + b, 0) / counted.length) * 100; } function overallOf(services: StatusService[]) { let worst: ServiceDayStatus = "operational"; for (const s of services) { const today = s.days[s.days.length - 1]?.status ?? "operational"; if (SEVERITY.indexOf(today) > SEVERITY.indexOf(worst)) worst = today; } return worst; } const BANNER: Record< ServiceDayStatus, { text: string; icon: ReactNode; className: string } > = { operational: { text: "All systems operational", icon: