/engineering/alertsAlerts 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.
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
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>
)
}Install
npx shadcn@latest add @vibra/engineering-alertsNeeds the @vibra registry in your components.json — set it up once.
Source
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>
)
}/**
* 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 }
}"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) }
}/**
* 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`
}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>
)
}"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>
)
}"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>
)
}