On call
Who holds the pager — the primary, the secondary and the escalation — and how many incidents each has commanded this quarter; reads onCall().
Preview
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { DataList, DataListItem } from "@/components/ui/data-list"
import { Widget } from "@/components/ui/widget"
import { type OnCallShift } from "../data"
/** Who is holding the pager, and what each of them has been carrying. */
export function OnCallCard({ shifts }: { shifts: OnCallShift[] }) {
return (
<Widget
data-widget="widget-engineering-incidents-on-call-card"
title="On call"
description="A new incident pages the primary first."
contentClassName="px-0"
>
<DataList divided>
{shifts.map((shift) => (
<DataListItem
key={shift.shift}
className="px-6"
leading={
<Avatar size="sm">
{shift.avatarUrl ? <AvatarImage src={shift.avatarUrl} alt="" /> : null}
<AvatarFallback className="text-2xs">{shift.initials}</AvatarFallback>
</Avatar>
}
title={shift.name}
description={shift.note}
meta={<Badge variant="outline">{shift.shift}</Badge>}
/>
))}
</DataList>
</Widget>
)
}Install
$
npx shadcn@latest add @vibra/widget-engineering-incidents-on-call-cardNeeds the @vibra registry in your components.json — set it up once.
Source
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { DataList, DataListItem } from "@/components/ui/data-list"
import { Widget } from "@/components/ui/widget"
import { type OnCallShift } from "../data"
/** Who is holding the pager, and what each of them has been carrying. */
export function OnCallCard({ shifts }: { shifts: OnCallShift[] }) {
return (
<Widget
data-widget="widget-engineering-incidents-on-call-card"
title="On call"
description="A new incident pages the primary first."
contentClassName="px-0"
>
<DataList divided>
{shifts.map((shift) => (
<DataListItem
key={shift.shift}
className="px-6"
leading={
<Avatar size="sm">
{shift.avatarUrl ? <AvatarImage src={shift.avatarUrl} alt="" /> : null}
<AvatarFallback className="text-2xs">{shift.initials}</AvatarFallback>
</Avatar>
}
title={shift.name}
description={shift.note}
meta={<Badge variant="outline">{shift.shift}</Badge>}
/>
))}
</DataList>
</Widget>
)
}/**
* What this page reads. Incidents, the services they took with them, and the
* people who ran them all come from `db` — `db.incidents`, `db.services` and
* `db.members` — and nothing here is written out by hand. The on-call rota is
* a rule over those rows rather than a rota table the sample data does not
* have: whoever is commanding the newest open incident is holding the pager.
* "Now" is `REFERENCE_DATE`.
*/
import { getInitials } from "@/lib/format"
import { db, REFERENCE_DATE, type Incident, type Member } from "@/lib/sample-data"
const SERVICES = db.services.all()
const SERVICE_NAME_BY_ID = new Map(SERVICES.map((service) => [service.id, service.name]))
// Read per call, never held at module scope: a member renamed since the
// server started is the name on the next render.
const membersById = () => new Map(db.members.all().map((member) => [member.id, member]))
const DAY_MS = 86_400_000
/** The internal severity scale, kept in the data's own words: responders say SEV1. */
export type Severity = Incident["severity"]
export type IncidentUpdate = { at: Date; status: Incident["status"]; message: string }
export type IncidentRow = {
id: string
title: string
severity: Severity
status: Incident["status"]
startedAt: Date
resolvedAt?: Date
/** Service names, not ids — the ids never reach the page. */
affected: string[]
/** The commander's display name, resolved through db.members. */
commander: string
commanderAvatar?: string
/** Newest first: where it got to, then how it got there. */
updates: IncidentUpdate[]
}
/** One incident as this page reads it. Exported because the declare action returns one. */
export function toRow(incident: Incident, members = membersById()): IncidentRow {
const commander = members.get(incident.commander)
return {
id: incident.id,
title: incident.title,
severity: incident.severity,
status: incident.status,
startedAt: incident.startedAt,
resolvedAt: incident.resolvedAt,
affected: incident.serviceIds.map((id) => SERVICE_NAME_BY_ID.get(id) ?? id),
commander: commander?.name ?? "Unassigned",
commanderAvatar: commander?.avatarUrl,
updates: [...incident.updates].sort((a, b) => b.at.getTime() - a.at.getTime()),
}
}
const isOpen = (incident: Incident) => incident.status !== "resolved"
/** Everything on the books: still running first, then whatever started most recently. */
export function incidents(): IncidentRow[] {
const members = membersById()
return [...db.incidents.all()]
.sort((a, b) => {
if (isOpen(a) !== isOpen(b)) return isOpen(a) ? -1 : 1
return b.startedAt.getTime() - a.startedAt.getTime()
})
.map((incident) => toRow(incident, members))
}
/** The line under the title: what is running now, against what the window holds. */
export function incidentSummary(): string {
const rows = db.incidents.all()
const open = rows.filter(isOpen).length
const oldest = Math.min(...rows.map((incident) => incident.startedAt.getTime()))
const months = Math.round((REFERENCE_DATE.getTime() - oldest) / (30 * DAY_MS))
return `${open} running · ${rows.length} over the last ${months} months`
}
// Whoever is commanding the newest open incident is the one actually holding
// the pager; the rota itself is not something the sample data records. Every
// fallback below is a real possibility for a workspace that has no incidents
// yet, or none of its members left active.
function primaryMember(): Member | undefined {
const newestOpen = [...db.incidents.all()]
.filter(isOpen)
.sort((a, b) => b.startedAt.getTime() - a.startedAt.getTime())[0]
const commander = newestOpen ? membersById().get(newestOpen.commander) : undefined
const active = db.members.all().filter((member) => member.status === "active")
return commander ?? active[0] ?? db.members.all()[0]
}
/**
* The id a newly declared incident is assigned to. Empty when the workspace
* has nobody to assign it to, which `toRow` reads back as "Unassigned" rather
* than inventing a commander.
*/
export function onCallPrimaryId(): string {
return primaryMember()?.id ?? ""
}
export type OnCallShift = {
shift: "Primary" | "Secondary" | "Escalation"
name: string
email: string
initials: string
avatarUrl?: string
/** What this person has actually been carrying, counted off db.incidents. */
note: string
}
const QUARTER_DAYS = 90
function commandedThisQuarter(memberId: string): number {
return db.incidents
.all()
.filter(
(incident) =>
incident.commander === memberId &&
REFERENCE_DATE.getTime() - incident.startedAt.getTime() < QUARTER_DAYS * DAY_MS
).length
}
function toShift(member: Member, shift: OnCallShift["shift"]): OnCallShift {
const commanded = commandedThisQuarter(member.id)
return {
shift,
name: member.name,
email: member.email,
initials: getInitials(member.name),
avatarUrl: member.avatarUrl,
note:
commanded === 0
? "No incidents this quarter"
: `${commanded} ${commanded === 1 ? "incident" : "incidents"} this quarter`,
}
}
/**
* Who is holding the pager, who backs them up, and who they wake at 3am.
* Nobody appears twice: a rota that lists the same person as their own backup
* is worse than a rota with two names on it, so a shift with nobody left to
* fill it is left off rather than filled with a repeat.
*/
export function onCall(): OnCallShift[] {
const primary = primaryMember()
if (!primary) return []
const taken = new Set([primary.id])
const active = db.members.all().filter((member) => member.status === "active")
// Prefer the role the shift wants, then anyone still unclaimed.
const claim = (role: Member["role"]): Member | undefined => {
const next =
active.find((member) => !taken.has(member.id) && member.role === role) ??
active.find((member) => !taken.has(member.id))
if (next) taken.add(next.id)
return next
}
const secondary = claim("admin")
const escalation = claim("owner")
return [
toShift(primary, "Primary"),
...(secondary ? [toShift(secondary, "Secondary")] : []),
...(escalation ? [toShift(escalation, "Escalation")] : []),
]
}
/** 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 }
}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 Incident console page