Infrastructure & DevOpsSections

Status Page

A public status page with an overall banner, 90-day per-component uptime bars, and an incident history feed.

Install

npx shadcn@latest add @paragon/status-page

status-page.tsx

"use client";

import * as React from "react";
import { CheckCircle2, AlertTriangle, Wrench } from "lucide-react";
import { cn } from "@/lib/utils";

export type ComponentState = "operational" | "degraded" | "outage" | "maintenance";

export interface StatusComponent {
  name: string;
  state: ComponentState;
  /** 90 daily uptime fractions, oldest → newest, 0–1. */
  history: number[];
  uptime: string;
}

export interface StatusIncident {
  date: string;
  title: string;
  severity: "resolved" | "degraded" | "outage" | "maintenance";
  summary: string;
}

export interface StatusPageProps extends React.ComponentProps<"div"> {
  title?: string;
  components?: StatusComponent[];
  incidents?: StatusIncident[];
}

const STATE_META: Record<
  ComponentState,
  { label: string; color: string }
> = {
  operational: { label: "Operational", color: "var(--color-success)" },
  degraded: { label: "Degraded", color: "var(--color-warning)" },
  outage: { label: "Outage", color: "var(--color-destructive)" },
  maintenance: { label: "Maintenance", color: "var(--color-muted-foreground)" },
};

function dayColor(v: number) {
  if (v >= 0.999) return "var(--color-success)";
  if (v >= 0.98) return "var(--color-warning)";
  return "var(--color-destructive)";
}

// Deterministic 90-day history, mostly perfect with a few dips.
function history(dips: Record<number, number> = {}): number[] {
  return Array.from({ length: 90 }, (_, i) => dips[i] ?? 1);
}

const DEFAULT_COMPONENTS: StatusComponent[] = [
  { name: "API", state: "operational", uptime: "99.98%", history: history({ 61: 0.94 }) },
  { name: "Dashboard", state: "operational", uptime: "100%", history: history() },
  {
    name: "Webhooks",
    state: "degraded",
    uptime: "99.71%",
    history: history({ 84: 0.9, 85: 0.86, 88: 0.97 }),
  },
  { name: "CDN", state: "operational", uptime: "99.99%", history: history({ 40: 0.995 }) },
  {
    name: "Payments",
    state: "operational",
    uptime: "99.95%",
    history: history({ 12: 0.97, 13: 0.99 }),
  },
];

const DEFAULT_INCIDENTS: StatusIncident[] = [
  {
    date: "Jul 5, 2026",
    title: "Elevated webhook delivery latency",
    severity: "degraded",
    summary:
      "A backlog in the delivery queue delayed webhooks by up to 4 minutes. Drained and monitoring.",
  },
  {
    date: "Jun 22, 2026",
    title: "Scheduled Postgres maintenance",
    severity: "maintenance",
    summary:
      "Primary failover completed with < 30s of read-only time. No data loss.",
  },
  {
    date: "Jun 3, 2026",
    title: "Partial API outage in us-east-1",
    severity: "outage",
    summary:
      "A bad deploy returned 5xx for ~6 minutes before automatic rollback. Post-mortem published.",
  },
];

function overallState(components: StatusComponent[]): ComponentState {
  if (components.some((c) => c.state === "outage")) return "outage";
  if (components.some((c) => c.state === "degraded")) return "degraded";
  if (components.some((c) => c.state === "maintenance")) return "maintenance";
  return "operational";
}

function IncidentIcon({ severity }: { severity: StatusIncident["severity"] }) {
  const cls = "size-4 shrink-0";
  if (severity === "resolved")
    return <CheckCircle2 className={cls} style={{ color: "var(--color-success)" }} />;
  if (severity === "maintenance")
    return <Wrench className={cls} style={{ color: "var(--color-muted-foreground)" }} />;
  const color =
    severity === "outage" ? "var(--color-destructive)" : "var(--color-warning)";
  return <AlertTriangle className={cls} style={{ color }} />;
}

export function StatusPage({
  title = "Acme Cloud",
  components = DEFAULT_COMPONENTS,
  incidents = DEFAULT_INCIDENTS,
  className,
  ...props
}: StatusPageProps) {
  const overall = overallState(components);
  const meta = STATE_META[overall];

  return (
    <div
      className={cn(
        "mx-auto w-full max-w-2xl rounded-xl bg-card p-5 text-card-foreground shadow-border sm:p-6",
        className,
      )}
      {...props}
    >
      {/* banner */}
      <div
        className="flex items-center gap-3 rounded-lg px-4 py-3"
        style={{
          background: `color-mix(in oklch, ${meta.color} 12%, transparent)`,
          boxShadow: `inset 0 0 0 1px color-mix(in oklch, ${meta.color} 30%, transparent)`,
        }}
      >
        <span
          className="flex size-8 items-center justify-center rounded-full"
          style={{ background: meta.color }}
        >
          {overall === "operational" ? (
            <CheckCircle2 className="size-5 text-success-foreground" />
          ) : (
            <AlertTriangle className="size-5 text-warning-foreground" />
          )}
        </span>
        <div>
          <p className="text-sm font-semibold">{title}</p>
          <p className="text-xs" style={{ color: meta.color }}>
            {overall === "operational"
              ? "All systems operational"
              : `${meta.label} — some systems affected`}
          </p>
        </div>
      </div>

      {/* components */}
      <ul className="mt-5 flex flex-col gap-4">
        {components.map((c) => {
          const cm = STATE_META[c.state];
          return (
            <li key={c.name}>
              <div className="mb-1.5 flex items-center justify-between">
                <span className="flex items-center gap-2 text-sm font-medium">
                  <span
                    className="size-2 rounded-full"
                    style={{ background: cm.color }}
                    aria-hidden
                  />
                  {c.name}
                </span>
                <span className="text-xs font-medium" style={{ color: cm.color }}>
                  {cm.label}
                </span>
              </div>
              <div className="flex items-end gap-[2px]" aria-hidden>
                {c.history.map((v, i) => (
                  <span
                    key={i}
                    title={`Day ${i - 89}: ${(v * 100).toFixed(2)}%`}
                    className="h-6 flex-1 rounded-[1.5px]"
                    style={{ background: dayColor(v), minWidth: 2 }}
                  />
                ))}
              </div>
              <div className="mt-1 flex justify-between text-[11px] text-muted-foreground">
                <span>90 days ago</span>
                <span className="tabular-nums">{c.uptime} uptime</span>
                <span>today</span>
              </div>
            </li>
          );
        })}
      </ul>

      {/* incidents */}
      <div className="mt-6 border-t border-border pt-4">
        <h3 className="text-sm font-semibold">Incident history</h3>
        <ul className="mt-3 flex flex-col gap-3">
          {incidents.map((inc, i) => (
            <li key={i} className="flex gap-2.5">
              <IncidentIcon severity={inc.severity} />
              <div>
                <div className="flex flex-wrap items-baseline gap-x-2">
                  <span className="text-sm font-medium">{inc.title}</span>
                  <span className="text-[11px] tabular-nums text-muted-foreground">
                    {inc.date}
                  </span>
                </div>
                <p className="text-xs text-muted-foreground">{inc.summary}</p>
              </div>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}