Skip to contentVibraUI

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

Install

npx shadcn@latest add @vibra/widget-engineering-incidents-on-call-card

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

Source

app/engineering/incidents/components/on-call-card.tsx
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>
  )
}
app/engineering/incidents/data.ts
/**
 * 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