Skip to contentVibraUI

Past incidents

The last fourteen days of closed incidents with the updates their responders published, each run of quiet days folded into one line; reads incidentHistory().

Preview

Install

npx shadcn@latest add @vibra/widget-engineering-status-incident-history

Needs the @vibra registry in your components.json — set it up once.

Source

app/status/components/incident-history.tsx
import { incidentHistory, type IncidentDay } from "../data"

import { IncidentEntry } from "./incident-entry"

/**
 * One entry in the rendered history: either a day that had incidents, or the
 * run of quiet days between two of them.
 */
type HistoryEntry =
  | { kind: "day"; day: IncidentDay }
  | { kind: "quiet"; key: string; from: string; to: string; days: number }

/**
 * Collapses consecutive quiet days into one line.
 *
 * Publishing the quiet days is what makes the loud ones credible — but
 * fourteen rows of "No incidents reported." is fourteen rows of nothing, and it
 * buries the two days that mattered. One line saying how long the quiet ran
 * makes the same claim in a twentieth of the height. The days arrive newest
 * first, so a run reads oldest-to-newest: "No incidents Aug 22 – Sep 4".
 */
export function collapseQuietDays(days: IncidentDay[]): HistoryEntry[] {
  const out: HistoryEntry[] = []
  for (let i = 0; i < days.length; ) {
    if (days[i].incidents.length > 0) {
      out.push({ kind: "day", day: days[i] })
      i += 1
      continue
    }
    let end = i
    while (end + 1 < days.length && days[end + 1].incidents.length === 0) end += 1
    out.push({
      kind: "quiet",
      key: `quiet-${days[i].key}`,
      // days[end] is the oldest of the run, days[i] the newest.
      from: days[end].label,
      to: days[i].label,
      days: end - i + 1,
    })
    i = end + 1
  }
  return out
}

/**
 * The last fortnight: a line for every day that had an incident, and one line
 * for each stretch of days that did not.
 */
export function IncidentHistory() {
  const entries = collapseQuietDays(incidentHistory())

  return (
    <ol data-widget="widget-engineering-status-incident-history" className="flex flex-col divide-y divide-rule panel">
      {entries.map((entry) =>
        entry.kind === "quiet" ? (
          <li key={entry.key} className="px-4 py-3">
            <p className="text-sm text-muted-foreground">
              {entry.days === 1
                ? `No incidents ${entry.to}.`
                : `No incidents ${entry.from} – ${entry.to}.`}
            </p>
          </li>
        ) : (
          <li key={entry.day.key} className="flex flex-col gap-3 px-4 py-3.5">
            <h3 className="text-sm font-medium">{entry.day.label}</h3>
            {entry.day.incidents.map((incident) => (
              <IncidentEntry key={incident.id} incident={incident} />
            ))}
          </li>
        )
      )}
    </ol>
  )
}
app/status/data.ts
/**
 * What the public status page reads. Everything comes from `db`: the services
 * and their 90 days of uptime from `db.services`, the incidents and the update
 * messages under them from `db.incidents`. Nothing is written here — a status
 * page is a read of the platform, not a control over it — so this file is
 * selectors alone, and "now" is `REFERENCE_DATE`.
 */
import { db, REFERENCE_DATE, type Incident, type Service } from "@/lib/sample-data"
import { type IncidentSeverity } from "@/components/ui/incident-list"
import { type ServiceStatus } from "@/components/ui/system-status"
import { type UptimeDay } from "@/components/ui/uptime-bar"

// The services are a read-only record; the incidents are read per call, never
// held at module scope — one declared on the incidents page since the server
// started is on this page the next time it is asked for.
const SERVICES = db.services.all()
const incidents = (): Incident[] => db.incidents.all()

const DAY_MS = 86_400_000

/**
 * Every date and time on this page is written in UTC, because the page says so
 * under its own title and because a reader comparing an incident against their
 * own logs needs one zone to compare in. These four are the only formatters the
 * block uses: a formatter with no `timeZone` renders in whatever zone the
 * browser is in, which west of UTC files September 4th's incidents under a
 * heading that reads September 3rd.
 */
const DAY_HEADING = new Intl.DateTimeFormat("en-US", { dateStyle: "long", timeZone: "UTC" })
const DATE_ONLY = new Intl.DateTimeFormat("en-US", { dateStyle: "medium", timeZone: "UTC" })
const TIME_ONLY = new Intl.DateTimeFormat("en-US", {
  hour: "2-digit",
  minute: "2-digit",
  hourCycle: "h23",
  timeZone: "UTC",
})
const STAMP = new Intl.DateTimeFormat("en-US", {
  month: "short",
  day: "numeric",
  hour: "2-digit",
  minute: "2-digit",
  hourCycle: "h23",
  timeZone: "UTC",
})

/** The heading over one day of incident history: "September 4, 2026". */
export function formatDayHeading(at: Date): string {
  return DAY_HEADING.format(at)
}

/** A date on its own, for the legend under the uptime strips. */
export function formatUtcDate(at: Date): string {
  return DATE_ONLY.format(at)
}

/** A clock time, for an update read under a heading that already gives the day. */
export function formatUtcTime(at: Date): string {
  return `${TIME_ONLY.format(at)} UTC`
}

/**
 * A date and a time. An incident that starts near midnight publishes its next
 * update the following day, so every stamp on a timeline carries its date.
 */
export function formatUtcStamp(at: Date): string {
  return `${STAMP.format(at)} UTC`
}

// Five status words in the data, four on a status page: both kinds of outage
// read as "outage" to someone who cannot use the product either way.
const STATUS: Record<Service["status"], ServiceStatus> = {
  operational: "operational",
  degraded: "degraded",
  partial_outage: "outage",
  major_outage: "outage",
  maintenance: "maintenance",
}

// Worst first, so a region takes the status of its unhappiest service rather
// than an average that would hide it.
const SEVERITY_ORDER: ServiceStatus[] = ["outage", "degraded", "maintenance", "operational"]

export type StatusRegion = { name: string; status: ServiceStatus; description: string }

/**
 * Where the platform runs, worst region first. The banner above them takes the
 * worst status of the lot, so an outage in one region is never averaged away
 * by three healthy ones.
 */
export function regions(): StatusRegion[] {
  const names = [...new Set(SERVICES.map((service) => service.region))]

  return names
    .map((name) => {
      const here = SERVICES.filter((service) => service.region === name)
      const statuses = here.map((service) => STATUS[service.status])
      const status = SEVERITY_ORDER.find((candidate) => statuses.includes(candidate)) ?? "operational"
      return {
        name,
        status,
        description: `${here.length} ${here.length === 1 ? "service" : "services"}`,
      }
    })
    .sort((a, b) => SEVERITY_ORDER.indexOf(a.status) - SEVERITY_ORDER.indexOf(b.status))
}

/**
 * A day is up above 99.5%, degraded down to 90%, and down below that. The
 * floor is deliberately low for a public page: a red day is a promise that the
 * product was unusable, and an hour of elevated errors inside a working day is
 * degradation, not an outage. Say "down" too readily and every strip reads as
 * a wall of red that nobody trusts.
 */
function dayStatus(uptime: number): UptimeDay["status"] {
  if (uptime >= 99.5) return "up"
  if (uptime >= 90) return "degraded"
  return "down"
}

const DAY_LABELS: Record<UptimeDay["status"], (uptime: number) => string> = {
  up: () => "No incidents",
  degraded: (uptime) => `${uptime.toFixed(2)}% availability`,
  down: (uptime) => `${uptime.toFixed(2)}% availability`,
  none: () => "Not measured",
}

export type StatusService = {
  id: string
  name: string
  status: ServiceStatus
  /** The share of the 90 days that stayed up, as the strip's own headline. */
  uptime: number
  days: UptimeDay[]
}

/** Every service, in the order the platform is usually described. */
export function services(): StatusService[] {
  return SERVICES.map((service) => {
    const history = service.uptime90d
    const start = REFERENCE_DATE.getTime() - (history.length - 1) * DAY_MS
    const average = history.reduce((sum, day) => sum + day, 0) / history.length

    return {
      id: service.id,
      name: service.name,
      status: STATUS[service.status],
      // Stated rather than left to UptimeBar's own count of good and bad days:
      // this is the availability the probes actually measured.
      uptime: Math.round(average * 100) / 100,
      days: history.map((value, index) => {
        const status = dayStatus(value)
        return {
          date: new Date(start + index * DAY_MS).toISOString().slice(0, 10),
          status,
          label: DAY_LABELS[status](value),
        }
      }),
    }
  })
}

/** How long the strips above cover, for the line under them. */
export function historyWindow(): { from: string; to: string; days: number } {
  const days = SERVICES[0]?.uptime90d.length ?? 0
  return {
    from: formatUtcDate(new Date(REFERENCE_DATE.getTime() - (days - 1) * DAY_MS)),
    to: formatUtcDate(REFERENCE_DATE),
    days,
  }
}

const SEVERITY: Record<Incident["severity"], IncidentSeverity> = {
  sev1: "critical",
  sev2: "major",
  sev3: "minor",
}

const SERVICE_NAME_BY_ID = new Map(SERVICES.map((service) => [service.id, service.name]))

export type StatusUpdate = { at: Date; status: Incident["status"]; message: string }

export type StatusIncident = {
  id: string
  title: string
  severity: IncidentSeverity
  status: Incident["status"]
  startedAt: Date
  resolvedAt?: Date
  affected: string[]
  updates: StatusUpdate[]
}

function toStatusIncident(incident: Incident): StatusIncident {
  return {
    id: incident.id,
    title: incident.title,
    severity: SEVERITY[incident.severity],
    status: incident.status,
    startedAt: incident.startedAt,
    resolvedAt: incident.resolvedAt,
    affected: incident.serviceIds.map((id) => SERVICE_NAME_BY_ID.get(id) ?? id),
    // Newest first: the reader wants where it got to, then how it got there.
    updates: [...incident.updates].sort((a, b) => b.at.getTime() - a.at.getTime()),
  }
}

/** Everything still open, newest first. */
export function activeIncidents(): StatusIncident[] {
  return incidents().filter((incident) => incident.status !== "resolved")
    .sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime())
    .map(toStatusIncident)
}

/** How many days of history the page publishes. */
export const HISTORY_DAYS = 14

export type IncidentDay = { key: string; label: string; incidents: StatusIncident[] }

/**
 * The last fourteen days, newest first, each with what closed on it. A day
 * with nothing on it still gets a row — that a status page says so is the
 * point of publishing one.
 */
export function incidentHistory(): IncidentDay[] {
  const closed = incidents().filter((incident) => incident.status === "resolved")
  const midnight = Date.UTC(
    REFERENCE_DATE.getUTCFullYear(),
    REFERENCE_DATE.getUTCMonth(),
    REFERENCE_DATE.getUTCDate()
  )

  return Array.from({ length: HISTORY_DAYS }, (_, index) => {
    const at = new Date(midnight - index * DAY_MS)
    const key = at.toISOString().slice(0, 10)
    return {
      key,
      label: formatDayHeading(at),
      incidents: closed
        .filter((incident) => incident.startedAt.toISOString().slice(0, 10) === key)
        .sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime())
        .map(toStatusIncident),
    }
  })
}
app/status/components/incident-entry.tsx
import {
  ActivityIcon,
  CircleAlertIcon,
  CircleCheckIcon,
  InfoIcon,
  OctagonAlertIcon,
  SearchIcon,
  TriangleAlertIcon,
  WrenchIcon,
  type LucideIcon,
} from "lucide-react"

import { formatDuration } from "@/lib/format"
import { type IncidentSeverity } from "@/components/ui/incident-list"
import { StatusBadge, type StatusVariant } from "@/components/ui/status-badge"
import { Timeline, TimelineItem, type TimelineTone } from "@/components/ui/timeline"

import { formatUtcStamp, formatUtcTime, type StatusIncident } from "../data"

// Severity and response stage in the pill's own tones. UI vocabulary, so both
// maps live with the component that draws them.
const SEVERITY_TONES: Record<IncidentSeverity, StatusVariant> = {
  critical: "danger",
  major: "warning",
  minor: "info",
  info: "neutral",
}

const SEVERITY_LABELS: Record<IncidentSeverity, string> = {
  critical: "Critical",
  major: "Major",
  minor: "Minor",
  info: "Info",
}

// Four different outlines, so severity still reads in greyscale — the same
// shapes IncidentList uses, so a badge means one thing across the product.
const SEVERITY_ICONS: Record<IncidentSeverity, LucideIcon> = {
  critical: OctagonAlertIcon,
  major: TriangleAlertIcon,
  minor: CircleAlertIcon,
  info: InfoIcon,
}

const STAGE_LABELS: Record<StatusIncident["status"], string> = {
  investigating: "Investigating",
  identified: "Identified",
  monitoring: "Monitoring",
  resolved: "Resolved",
}

const STAGE_TONES: Record<StatusIncident["status"], TimelineTone> = {
  investigating: "danger",
  identified: "warning",
  monitoring: "info",
  resolved: "success",
}

const STAGE_ICONS: Record<StatusIncident["status"], LucideIcon> = {
  investigating: SearchIcon,
  identified: WrenchIcon,
  monitoring: ActivityIcon,
  resolved: CircleCheckIcon,
}

export type IncidentEntryProps = {
  incident: StatusIncident
  /** An open incident is dated by the day it started; a closed one is under it already. */
  showDate?: boolean
  /** Whatever keeps the page's heading order intact where the entry is used. */
  titleAs?: "h3" | "h4"
}

/** One incident, with the updates its responders published under it. */
export function IncidentEntry({
  incident,
  showDate = false,
  titleAs: Title = "h4",
}: IncidentEntryProps) {
  const ran = incident.resolvedAt
    ? formatDuration(incident.resolvedAt.getTime() - incident.startedAt.getTime())
    : null
  const SeverityIcon = SEVERITY_ICONS[incident.severity]

  return (
    <article data-slot="incident-entry" className="flex flex-col gap-3">
      <div className="flex flex-col gap-1.5">
        <div className="flex flex-wrap items-center gap-2">
          <StatusBadge
            status={incident.severity}
            variant={SEVERITY_TONES[incident.severity]}
            dot={false}
            size="sm"
          >
            <SeverityIcon aria-hidden="true" className="size-3" />
            {SEVERITY_LABELS[incident.severity]}
          </StatusBadge>
          <Title className="text-sm font-medium text-pretty">{incident.title}</Title>
        </div>
        <p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
          {showDate ? (
            <span className="tabular-nums">{`Started ${formatUtcStamp(incident.startedAt)}`}</span>
          ) : (
            <span className="tabular-nums">{`Started ${formatUtcTime(incident.startedAt)}`}</span>
          )}
          {ran ? <span>{`Resolved in ${ran}`}</span> : null}
          {incident.affected.length > 0 ? (
            <span>
              <span className="sr-only">{"Affected: "}</span>
              {incident.affected.join(", ")}
            </span>
          ) : null}
        </p>
      </div>

      <Timeline>
        {incident.updates.map((update, index) => {
          const StageIcon = STAGE_ICONS[update.status]
          return (
            <TimelineItem
              key={`${incident.id}-${index}`}
              tone={STAGE_TONES[update.status]}
              icon={<StageIcon aria-hidden="true" />}
              title={STAGE_LABELS[update.status]}
              time={<span className="tabular-nums">{formatUtcStamp(update.at)}</span>}
              description={update.message}
            />
          )
        })}
      </Timeline>
    </article>
  )
}

Its page

On its page the card sits among the rest of the dashboard and shares its range and its data with them.

From the Public status page page