Skip to contentVibraUI
Part of the Engineering dashboardinstalls at /engineering/alerts

Alerts console

Every monitoring rule the workspace watches: what is firing as callouts at the top, the rules table under them with severity, state and the channel each one pages, and a silence action with a duration.

Open the live page

The page is a server component inside AppShell: it reads db.alerts and hands the rules to one client island, which owns the status filter, the silence window and the two actions. PeriodSelect offers reporting ranges by default; here the same control is relabelled as durations, because a silence is a window forward rather than a window back. Silencing is a server action that writes to db.alerts and hands the row back, and it refuses exactly one thing — a firing SEV1, which pages the on-call, and muting one is how an outage goes unnoticed; the refusal lands in a role="alert" callout where the reader is already looking rather than being swallowed. The window a rule is muted for is worked out from REFERENCE_DATE and reported rather than stored: Alert records a status, not an expiry. Composes AppShell, PageHeader, Callout, QuickFilters, PeriodSelect, Widget, SimpleTable, StatusBadge, Badge and RelativeTime.

Preview

Install

npx shadcn@latest add @vibra/engineering-alerts

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

Source

app/engineering/alerts/page.tsx
import { REFERENCE_DATE } from "@/lib/sample-data"
import { AppShell } from "@/components/ui/app-shell"
import { PageHeader } from "@/components/ui/page-header"

import { signOut } from "./actions"
import { AlertsView } from "./components/alerts-view"
import { alerts, alertSummary, currentUser, shellNotifications } from "./data"
import { NAV, ROUTES } from "@/lib/dashboards/engineering/nav"

/**
 * The alerts console. The page is a server component: it reads every rule
 * through `db` and hands them to one client island, which owns the status
 * filter, the silence window and the two actions that change anything.
 */
export default function AlertsPage() {
  return (
    <AppShell
      nav={NAV}
      activeHref={ROUTES.alerts}
      user={currentUser()}
      notifications={shellNotifications()}
      now={REFERENCE_DATE}
      onSignOut={signOut}
    >
      <PageHeader
        title="Alerts"
        description="Every rule the workspace watches, and which of them are going off."
        meta={alertSummary()}
      />

      <AlertsView rows={alerts()} now={REFERENCE_DATE} />
    </AppShell>
  )
}
app/engineering/alerts/data.ts
/**
 * What this page reads. Every rule, its condition, where it pages and when it
 * last fired comes from `db.alerts`; nothing here is written out. "Now" is
 * `REFERENCE_DATE`, which is what the silence windows are measured from.
 */
import { getInitials } from "@/lib/format"
import { db, type Alert, type Member } from "@/lib/sample-data"

export type AlertStatus = Alert["status"]
export type AlertSeverity = Alert["severity"]
export type AlertChannel = Alert["channel"]

export type AlertRow = {
  id: string
  name: string
  condition: string
  severity: AlertSeverity
  status: AlertStatus
  firedAt?: Date
  /** When the quiet runs out; only a silenced rule has one. */
  silencedUntil?: Date
  channel: AlertChannel
}

/** One rule as this page reads it. Exported because the silence actions return one. */
export function toRow(alert: Alert): AlertRow {
  const { id, name, condition, severity, status, firedAt, channel } = alert
  return {
    id,
    name,
    condition,
    severity,
    status,
    firedAt,
    // The row keeps the window of the last silence after it is lifted, as the
    // record of it; only a rule that is still silenced is inside one.
    silencedUntil: status === "silenced" ? alert.silencedUntil : undefined,
    channel,
  }
}

// Worst first, then whatever fired most recently: a page of rules is read
// top-down under pressure, so the order has to be the order of attention.
const SEVERITY_RANK: Record<AlertSeverity, number> = { sev1: 0, sev2: 1, sev3: 2 }
const STATUS_RANK: Record<AlertStatus, number> = { firing: 0, silenced: 1, resolved: 2 }

function compare(a: Alert, b: Alert): number {
  if (STATUS_RANK[a.status] !== STATUS_RANK[b.status])
    return STATUS_RANK[a.status] - STATUS_RANK[b.status]
  if (SEVERITY_RANK[a.severity] !== SEVERITY_RANK[b.severity])
    return SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]
  return (b.firedAt?.getTime() ?? 0) - (a.firedAt?.getTime() ?? 0)
}

/** Every rule the workspace has, firing first and worst first inside that. */
export function alerts(): AlertRow[] {
  return [...db.alerts.all()].sort(compare).map(toRow)
}

/** The line under the title: what is loud now, against how many rules there are. */
export function alertSummary(): string {
  const rows = db.alerts.all()
  const count = (status: AlertStatus) => rows.filter((alert) => alert.status === status).length
  return `${count("firing")} firing · ${count("silenced")} silenced · ${rows.length} rules`
}

/** The bell's contents: the newest notifications, unread first in the panel. */
export function shellNotifications() {
  return db.notifications
    .all()
    .sort((a, b) => b.at.getTime() - a.at.getTime())
    .slice(0, 6)
    .map(({ id, title, description, at, read, href }) => ({ id, title, description, at, read, href }))
}

function ownerRow(): Member {
  return db.members.all().find((member) => member.role === "owner") ?? db.members.all()[0]
}

/** The person looking at the page: whoever owns this workspace. */
export function currentUser() {
  const owner = ownerRow()
  return { name: owner.name, email: owner.email, initials: getInitials(owner.name), avatarUrl: owner.avatarUrl }
}
app/engineering/alerts/actions.ts
"use server"

import { mockAuthAdapter } from "@/lib/auth-adapter"
import { db, invalidInput, ownKey, REFERENCE_DATE, type Result } from "@/lib/sample-data"

import { toRow, type AlertRow, type AlertStatus } from "./data"
import { formatUntil } from "./vocabulary"

/**
 * The three things this page changes. Server actions, so the page can stay a
 * server component and still hand its client island something to call, and
 * `Result` so every caller reads one success-or-error shape.
 */

export async function signOut(): Promise<Result<{ signedOut: true }>> {
  await mockAuthAdapter.signOut()
  return { ok: true, data: { signedOut: true } }
}

/** The windows a rule can be muted for, in milliseconds. */
export type SilenceWindow = "24h" | "7d" | "30d"

const WINDOW_MS: Record<SilenceWindow, number> = {
  "24h": 86_400_000,
  "7d": 7 * 86_400_000,
  "30d": 30 * 86_400_000,
}

export type SilencedAlert = {
  alert: AlertRow
  /** When the silence runs out, read back off the row the write put it on. */
  until: Date
}

/**
 * Mutes a rule for a window. A firing SEV1 is the one thing this refuses:
 * those page the on-call, and muting one is how an outage goes unnoticed.
 */
export async function silenceAlert(
  id: string,
  window: SilenceWindow
): Promise<Result<SilencedAlert>> {
  // One of the three windows, read as WINDOW_MS's own key: anything else once
  // stored an Invalid Date, which the next silence of the rule threw on.
  if (!ownKey(WINDOW_MS, window)) return invalidInput("Silence it for 24 hours, 7 days or 30 days.", "window")
  const alert = await db.alerts.get(id)
  if (!alert) {
    return { ok: false, error: { code: "not_found", message: `No alert with id "${id}"` } }
  }

  if (alert.severity === "sev1" && alert.status === "firing") {
    return {
      ok: false,
      error: {
        code: "not_allowed",
        message: `"${alert.name}" is a firing SEV1 and pages the on-call. Declare an incident instead of silencing it.`,
      },
    }
  }

  if (alert.status === "silenced") {
    // The window the rule is already inside is the useful half of this: a
    // responder asking again wants to know when the quiet ends, not that
    // somebody got there first.
    const until = alert.silencedUntil
    return {
      ok: false,
      error: {
        code: "conflict",
        message: until
          ? `"${alert.name}" is already silenced until ${formatUntil(until)}.`
          : `"${alert.name}" is already silenced.`,
      },
    }
  }

  // The row keeps both facts about a silence: what it interrupted, so resuming
  // can put it back, and when it runs out.
  const updated = await db.alerts.update(id, {
    status: "silenced",
    silencedFrom: alert.status,
    silencedUntil: new Date(REFERENCE_DATE.getTime() + WINDOW_MS[window]),
  })
  if (!updated.ok) return updated

  return {
    ok: true,
    data: { alert: toRow(updated.data), until: updated.data.silencedUntil! },
  }
}

/** Puts a muted rule back on duty. */
export async function resumeAlert(id: string): Promise<Result<AlertRow>> {
  const alert = await db.alerts.get(id)
  if (!alert) {
    return { ok: false, error: { code: "not_found", message: `No alert with id "${id}"` } }
  }

  if (alert.status !== "silenced") {
    return {
      ok: false,
      error: { code: "conflict", message: `"${alert.name}" is not silenced.` },
    }
  }

  // Restore what it was, not what it looks like. Silencing a rule is not
  // supposed to change it, so resuming one hands back the status the row
  // recorded when it was muted — a resolved rule that someone quietened must
  // not come back firing, and a firing one must not come back resolved. Every
  // silenced row carries that, seeded ones included, so there is nothing left
  // to remember in this process.
  const next: AlertStatus = alert.silencedFrom ?? "resolved"

  const updated = await db.alerts.update(id, { status: next })
  if (!updated.ok) return updated

  return { ok: true, data: toRow(updated.data) }
}
app/engineering/alerts/vocabulary.ts
/**
 * How this console writes a silence window. Vocabulary, not data — it reads
 * no `db` — so the island that reports a silence imports it from here rather
 * than from `data.ts`, which reads the store at module scope and must never
 * reach the browser. It sits at the page root, not in `components/`, because
 * the silence action says it too, and a server file never imports from a
 * block's `components/` folder.
 */

// Fixed to UTC so a silence window reads the same for two responders in two
// time zones, and formatted here because both the action and the page say it.
const UNTIL = new Intl.DateTimeFormat("en-US", {
  dateStyle: "medium",
  timeStyle: "short",
  hourCycle: "h23",
  timeZone: "UTC",
})

/** How a silence window is written once it has been worked out. */
export function formatUntil(at: Date): string {
  return `${UNTIL.format(at)} UTC`
}
app/engineering/alerts/components/alert-vocabulary.tsx
import {
  CircleAlertIcon,
  MailIcon,
  MessageSquareIcon,
  OctagonAlertIcon,
  SirenIcon,
  TriangleAlertIcon,
  type LucideIcon,
} from "lucide-react"

import { Badge } from "@/components/ui/badge"
import { StatusBadge, type StatusVariant } from "@/components/ui/status-badge"

import { type AlertChannel, type AlertSeverity, type AlertStatus } from "../data"

/**
 * How this console writes severity, state and channel. Responders say SEV1, so
 * the internal pages keep the data's own words; the public status page is the
 * one that translates them. UI vocabulary, so it lives with the components
 * that draw it rather than in data.ts.
 */

export const SEVERITY_LABELS: Record<AlertSeverity, string> = {
  sev1: "SEV1",
  sev2: "SEV2",
  sev3: "SEV3",
}

const SEVERITY_TONES: Record<AlertSeverity, StatusVariant> = {
  sev1: "danger",
  sev2: "warning",
  sev3: "info",
}

/** The tone a severity is painted in, so a caller can tell when it is about to repeat itself. */
export function severityTone(severity: AlertSeverity): StatusVariant {
  return SEVERITY_TONES[severity]
}

// A pill inside a callout of its own tone would be a wash on a wash: two 15%
// tints of one hue, with the label reading against both. Solid is the way out
// — the pill keeps its shape and its text sits on an opaque ground.
const SEVERITY_SOLID: Record<AlertSeverity, string> = {
  sev1: "bg-danger text-danger-foreground",
  sev2: "bg-warning text-warning-foreground",
  sev3: "bg-info text-info-foreground",
}

// Three outlines, so severity still reads in greyscale.
const SEVERITY_ICONS: Record<AlertSeverity, LucideIcon> = {
  sev1: OctagonAlertIcon,
  sev2: TriangleAlertIcon,
  sev3: CircleAlertIcon,
}

export const STATUS_LABELS: Record<AlertStatus, string> = {
  firing: "Firing",
  silenced: "Silenced",
  resolved: "Resolved",
}

export const STATUSES = Object.keys(STATUS_LABELS) as AlertStatus[]

const STATUS_TONES: Record<AlertStatus, StatusVariant> = {
  firing: "danger",
  silenced: "neutral",
  resolved: "success",
}

export const CHANNEL_LABELS: Record<AlertChannel, string> = {
  chat: "Chat",
  pager: "On-call pager",
  email: "Email",
}

const CHANNEL_ICONS: Record<AlertChannel, LucideIcon> = {
  chat: MessageSquareIcon,
  pager: SirenIcon,
  email: MailIcon,
}

/** How loud a rule is when it goes off. */
export function SeverityBadge({
  severity,
  solid = false,
}: {
  severity: AlertSeverity
  /** Paints the pill on an opaque ground, for a surface already washed in its tone. */
  solid?: boolean
}) {
  const Icon = SEVERITY_ICONS[severity]
  return (
    <StatusBadge
      status={severity}
      variant={SEVERITY_TONES[severity]}
      dot={false}
      size="sm"
      data-solid={solid || undefined}
      className={solid ? SEVERITY_SOLID[severity] : undefined}
    >
      <Icon aria-hidden="true" className="size-3" />
      {SEVERITY_LABELS[severity]}
    </StatusBadge>
  )
}

/** Where the rule stands right now. */
export function AlertStatusBadge({ status }: { status: AlertStatus }) {
  return (
    <StatusBadge
      status={status}
      variant={STATUS_TONES[status]}
      label={STATUS_LABELS[status]}
      size="sm"
      // A rule that is going off is a state that is actively changing.
      pulse={status === "firing"}
    />
  )
}

/** Who gets woken when it does. */
export function ChannelBadge({ channel }: { channel: AlertChannel }) {
  const Icon = CHANNEL_ICONS[channel]
  return (
    <Badge variant="outline" className="text-muted-foreground">
      <Icon aria-hidden="true" />
      {CHANNEL_LABELS[channel]}
    </Badge>
  )
}
app/engineering/alerts/components/alerts-view.tsx
"use client"

import * as React from "react"
import { CircleCheckIcon, TriangleAlertIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { Callout } from "@/components/ui/callout"
import { PeriodSelect, type Period, type PeriodOption } from "@/components/ui/period-select"
import { QuickFilters } from "@/components/ui/quick-filters"
import { RelativeTime } from "@/components/ui/relative-time"
import { SimpleTable, type SimpleTableColumn } from "@/components/ui/simple-table"
import { Widget } from "@/components/ui/widget"

import { resumeAlert, silenceAlert, type SilenceWindow } from "../actions"
import { type AlertRow, type AlertStatus } from "../data"
import { formatUntil } from "../vocabulary"

import {
  AlertStatusBadge,
  ChannelBadge,
  SeverityBadge,
  STATUS_LABELS,
  STATUSES,
} from "./alert-vocabulary"
import { FiringCallouts } from "./firing-callouts"

// How long a rule can be muted for. PeriodSelect's windows are reporting
// ranges by default; here they are durations, so they are relabelled.
const DURATIONS: PeriodOption[] = [
  { value: "24h", label: "24 hours" },
  { value: "7d", label: "7 days" },
  { value: "30d", label: "30 days" },
]

const DURATION_LABELS: Record<SilenceWindow, string> = {
  "24h": "24 hours",
  "7d": "7 days",
  "30d": "30 days",
}

type Notice = { tone: "success" | "error"; message: string }

export type AlertsViewProps = {
  rows: AlertRow[]
  /** What the stamps count from — REFERENCE_DATE, the same point the rows were generated from. */
  now: Date
}

/**
 * The alerts console. It holds the rules in state because silencing one writes
 * a real row, and the reader should see the change land without a navigation.
 */
export function AlertsView({ rows, now }: AlertsViewProps) {
  const [alerts, setAlerts] = React.useState(rows)
  const [status, setStatus] = React.useState<AlertStatus | "all">("all")
  const [duration, setDuration] = React.useState<SilenceWindow>("24h")
  const [notice, setNotice] = React.useState<Notice | null>(null)
  const [pendingId, setPendingId] = React.useState<string | null>(null)

  const visible = alerts.filter((alert) => status === "all" || alert.status === status)
  const firing = alerts.filter((alert) => alert.status === "firing")

  function replace(row: AlertRow) {
    setAlerts((current) => current.map((alert) => (alert.id === row.id ? row : alert)))
  }

  async function silence(alert: AlertRow) {
    setPendingId(alert.id)
    const result = await silenceAlert(alert.id, duration)
    setPendingId(null)
    if (!result.ok) {
      setNotice({ tone: "error", message: result.error.message })
      return
    }
    replace(result.data.alert)
    setNotice({
      tone: "success",
      message: `${alert.name} is silenced for ${DURATION_LABELS[duration]}, until ${formatUntil(result.data.until)}.`,
    })
  }

  async function resume(alert: AlertRow) {
    setPendingId(alert.id)
    const result = await resumeAlert(alert.id)
    setPendingId(null)
    if (!result.ok) {
      setNotice({ tone: "error", message: result.error.message })
      return
    }
    replace(result.data)
    setNotice({ tone: "success", message: `${alert.name} is back on duty.` })
  }

  const columns: SimpleTableColumn<AlertRow>[] = [
    {
      key: "name",
      header: "Rule",
      cell: (alert) => (
        <span className="flex min-w-0 flex-col gap-0.5">
          <span className="font-medium">{alert.name}</span>
          {/* The id leads the condition because db.alerts holds several rules
              with the same name, and a silence quoted in a runbook has to name
              exactly one of them. */}
          <span className="font-mono text-xs text-muted-foreground">
            {`${alert.id} · ${alert.condition}`}
          </span>
        </span>
      ),
    },
    {
      key: "severity",
      header: "Severity",
      cell: (alert) => <SeverityBadge severity={alert.severity} />,
    },
    {
      key: "status",
      header: "State",
      cell: (alert) => <AlertStatusBadge status={alert.status} />,
    },
    {
      key: "channel",
      header: "Pages",
      cell: (alert) => <ChannelBadge channel={alert.channel} />,
    },
    {
      key: "firedAt",
      header: "Last fired",
      align: "right",
      cell: (alert) =>
        alert.firedAt ? (
          // Frozen, and counted from REFERENCE_DATE: a record of when it
          // went off, not a clock.
          <RelativeTime date={alert.firedAt} now={now} updateInterval={null} />
        ) : (
          <span className="text-muted-foreground">Never</span>
        ),
    },
    {
      key: "actions",
      header: <span className="sr-only">Actions</span>,
      align: "right",
      cell: (alert) =>
        alert.status === "silenced" ? (
          // Both row actions are quiet: the loud one is on the callout above,
          // and thirty bordered buttons would read as thirty decisions.
          <Button
            type="button"
            variant="ghost"
            size="sm"
            disabled={pendingId === alert.id}
            onClick={() => void resume(alert)}
          >
            Resume
          </Button>
        ) : (
          <Button
            type="button"
            variant="ghost"
            size="sm"
            disabled={pendingId === alert.id}
            onClick={() => void silence(alert)}
          >
            Silence
          </Button>
        ),
    },
  ]

  return (
    <div className="flex flex-col gap-4">
      <FiringCallouts
        alerts={firing}
        now={now}
        onSilence={(alert) => void silence(alert)}
        pendingId={pendingId}
      />

      {notice ? (
        <Callout
          // A rejected action is an alert; a completed one is a status. Both
          // land where the reader is already looking.
          role={notice.tone === "error" ? "alert" : "status"}
          variant={notice.tone === "error" ? "danger" : "success"}
          icon={
            notice.tone === "error" ? (
              <TriangleAlertIcon aria-hidden="true" />
            ) : (
              <CircleCheckIcon aria-hidden="true" />
            )
          }
        >
          {notice.message}
        </Callout>
      ) : null}

      <div className="flex flex-wrap items-center justify-between gap-3">
        <QuickFilters
          aria-label="Alert status"
          size="sm"
          value={status}
          onValueChange={(next) => setStatus(next as AlertStatus | "all")}
          options={[
            { value: "all", label: "All", count: alerts.length },
            ...STATUSES.map((state) => ({
              value: state,
              label: STATUS_LABELS[state],
              count: alerts.filter((alert) => alert.status === state).length,
            })),
          ]}
        />

        <div className="flex items-center gap-2">
          <span className="text-sm text-muted-foreground">Silence for</span>
          <PeriodSelect
            aria-label="Silence duration"
            size="sm"
            allowCustom={false}
            options={DURATIONS}
            value={duration}
            onValueChange={(period: Period) => setDuration(period as SilenceWindow)}
          />
        </div>
      </div>

      {/* Widget is already role="region", named by its own title. */}
      <Widget
        title="Alert rules"
        description={`${visible.length} of ${alerts.length} shown, firing first.`}
        contentClassName="px-0"
      >
        <SimpleTable
          columns={columns}
          rows={visible}
          rowKey="id"
          size="sm"
          emptyMessage="No rules in this state."
          className="border-0"
        />
      </Widget>
    </div>
  )
}
app/engineering/alerts/components/firing-callouts.tsx
"use client"

import { Button } from "@/components/ui/button"
import { Callout } from "@/components/ui/callout"
import { RelativeTime } from "@/components/ui/relative-time"

import { type AlertRow } from "../data"

import { SeverityBadge, severityTone } from "./alert-vocabulary"

// Three is what fits above the fold without pushing the table off it; the rest
// are one scroll away in the table, and the header says how many there are.
const SHOWN = 3

export type FiringCalloutsProps = {
  alerts: AlertRow[]
  /** What the stamps count from — REFERENCE_DATE, passed down from the page. */
  now: Date
  onSilence: (alert: AlertRow) => void
  /** The rule the reader has just acted on, so its control can say so. */
  pendingId: string | null
}

/** What is going off right now, worst first. */
export function FiringCallouts({ alerts, now, onSilence, pendingId }: FiringCalloutsProps) {
  const shown = alerts.slice(0, SHOWN)

  return (
    <section aria-label="Firing now" className="flex flex-col gap-2">
      {shown.map((alert) => {
        // The callout is as loud as the rule it reports: a SEV1 in the danger
        // tone, everything else in warning.
        const tone = alert.severity === "sev1" ? "danger" : "warning"

        return (
          <Callout
            key={alert.id}
            variant={tone}
            icon={null}
            title={
              <span className="flex flex-wrap items-center gap-2">
                {/* On a surface already washed in the pill's own tone, the pill
                    goes solid rather than laying a second wash over the first. */}
                <SeverityBadge
                  severity={alert.severity}
                  solid={severityTone(alert.severity) === tone}
                />
                {alert.name}
              </span>
            }
          >
            <div className="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
              <span className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
                <code className="font-mono text-xs">{alert.condition}</code>
                {alert.firedAt ? (
                  <span className="text-xs text-muted-foreground">
                    {"Firing since "}
                    {/* Frozen, and counted from REFERENCE_DATE: a record of
                        when it went off, not a clock. */}
                    <RelativeTime date={alert.firedAt} now={now} updateInterval={null} />
                  </span>
                ) : null}
              </span>
              <Button
                type="button"
                variant="outline"
                size="sm"
                disabled={pendingId === alert.id}
                onClick={() => onSilence(alert)}
              >
                Silence
              </Button>
            </div>
          </Callout>
        )
      })}

      {alerts.length > shown.length ? (
        <p className="text-xs text-muted-foreground">
          {`${alerts.length - shown.length} more firing, in the table below.`}
        </p>
      ) : null}

      {alerts.length === 0 ? (
        <p className="panel px-4 py-6 text-center text-sm text-muted-foreground">
          Nothing is firing.
        </p>
      ) : null}
    </section>
  )
}