Files
cameleer-server/docs/superpowers/specs/2026-04-21-alerts-design-system-alignment-design.md
hsiegeln 3d0a4d289b docs(alerts): Design spec — design-system alignment for /alerts pages
Rework all pages under /alerts to use @cameleer/design-system components
and tokens. Unified DataTable shell for Inbox/All/History with expandable
rows; DataTable + Dropdown + ConfirmDialog for Rules list; FormField grid
+ DataTable for Silences; DS Alert for wizard banners. Replaces undefined
CSS variables (--bg, --fg, --muted, --accent) with DS tokens and removes
raw <table>/<select>/confirm() usage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:43:19 +02:00

9.7 KiB

Alerts pages — design-system alignment

Status: Approved (2026-04-21) Scope: All pages and helper components under /alerts in ui/src/pages/Alerts/ plus ui/src/components/NotificationBell.tsx (audit only — already on DS). Non-goals: Backend changes, DS package changes, alert semantics, MustacheEditor restyling.

Problem

Pages under /alerts don't fully adhere to the @cameleer/design-system styling and component conventions used by the rest of the SPA (Admin, Audit, Apps, Runtime). Concretely:

  1. Undefined CSS variables. alerts-page.module.css and wizard.module.css use tokens (--bg, --fg, --muted, --accent) that are not defined by the DS (verified against @cameleer/design-system/dist/style.css). These fall back to browser defaults and do not theme correctly in dark mode. (Note: --border and --amber-bg are valid DS tokens, but --border-subtle is the convention used by the rest of the app for card chrome.)
  2. Raw HTML where DS components exist. Raw <table> (RulesList, Silences), raw <select> (RulesList promote), custom centered-div empty states, custom "promote banner" div.
  3. Inconsistent page layout. Toolbars built ad-hoc with inline styles. Admin / Audit pages use a consistent SectionHeader + sectionStyles.section / tableStyles.tableSection shell.
  4. Native confirm() instead of DS ConfirmDialog.

Design principles

  1. Consistency over novelty — all three list pages (Inbox / All / History) share one DataTable shell; they differ only in toolbar controls.
  2. Double-encode severity — DS SeverityBadge column and rowAccent tint — accessible to colorblind users.
  3. Expandable rows give the inbox-style preview affordance without needing a separate feed layout.
  4. Relative time (2m ago) with tooltip for absolute ISO — industry-standard for alert consoles.
  5. Use DS tokens only--bg-surface, --border-subtle, --radius-lg, --shadow-card, --text-primary/secondary/muted, --space-sm/md/lg.

Per-page design

Inbox (/alerts/inbox)

Personal triage queue — user-targeted FIRING/ACKNOWLEDGED alerts.

  • Shell: <SectionHeader>Inbox</SectionHeader> → bulk-action toolbar (Mark selected read, Mark all read) → tableStyles.tableSection wrapping DataTable.
  • Columns: ☐ checkbox | Severity | State | Title | App/Rule | Age | Ack.
  • rowAccent: map severity → error | warning | info. Unread (FIRING) rows render with DataTable's inherent accent tint; additional bold weight on title via render.
  • expandedContent: message body, targeted users, fireMode, absolute firedAt/updatedAt.
  • Empty state: DS <EmptyState icon={<Inbox />} title="All clear" description="No open alerts for you in this environment." />.

All alerts (/alerts/all)

Env-wide operational awareness.

  • Same shell as Inbox, minus the checkbox column.
  • Filter bar: DS ButtonGroup with items Open / Firing / Acked / All. Replaces the current four-Button row.
  • Columns: Severity | State | Title | App/Rule | Fired at | Silenced.
  • expandedContent: same as Inbox.
  • Empty state: EmptyState with filter-specific message.

History (/alerts/history)

Retrospective lookup — RESOLVED alerts only.

  • Same shell as All.
  • Filter bar: DS DateRangePicker (default: last 7 days). Replaces the static "retention window" label.
  • Columns: Severity | Title | App/Rule | Fired at | Resolved at | Duration.
  • expandedContent: message body, rule snapshot pointer, full timestamps.
  • Empty state: EmptyState with "No resolved alerts in selected range."

Rules list (/alerts/rules)

  • Shell: <SectionHeader action={<Button>New rule</Button>}> with DS action slot — replaces the inline flex container that currently wraps them.
  • Raw <table>DataTable inside tableStyles.tableSection.
  • Columns: Name (Link) | Kind (Badge) | Severity (SeverityBadge) | Enabled (Toggle) | Targets (count) | Actions.
  • Actions cell: DS Dropdown for Promote to env (replaces raw <select>), DS Button variant="ghost" Delete opening a ConfirmDialog.
  • Empty state: EmptyState with CTA linking to /alerts/rules/new.

Silences (/alerts/silences)

  • Shell: <SectionHeader>Alert silences</SectionHeader>.
  • Create form: kept in sectionStyles.section, but grid laid out via FormFields with proper Label and hint props — no inline-style grid.
  • List: raw <table>DataTable below the form.
  • Columns: Matcher (MonoText) | Reason | Starts | Ends | End action.
  • End action → ConfirmDialog.
  • Empty state: EmptyState "No active or scheduled silences."

Rule editor wizard (/alerts/rules/new, /alerts/rules/:id)

Keep the current custom tab stepper — DS has no Stepper, and the existing layout is appropriate.

Changes:

  • wizard.module.css — replace undefined tokens with DS tokens. .wizard uses --space-md gap; .steps underline uses --border-subtle; .stepActive border uses --amber (the DS accent color); .step idle color uses --text-muted, active/done uses --text-primary.
  • Promote banner → DS <Alert variant="info">.
  • Warnings block → DS <Alert variant="warning"> with the list as children.
  • Step body wraps in sectionStyles.section for card affordance matching other forms.

Shared changes

alerts-page.module.css

Reduced to layout-only:

.page {
  padding: var(--space-md);
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
}
.toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-sm);
}
.filterBar {
  display: flex;
  gap: var(--space-sm);
  align-items: center;
}

Delete: .row, .rowUnread, .body, .meta, .time, .message, .actions, .empty — all replaced by DS components.

AlertRow.tsx

Delete. Logic migrates into:

  • A column renderer for the Title cell (handles the Link + markRead side-effect on click).
  • An expandedContent renderer shared across the three list pages (extracted into ui/src/pages/Alerts/alert-expanded.tsx).
  • An Ack action button rendered via DataTable Actions column.

ConfirmDialog migration

Replaces native confirm() in RulesListPage (delete), SilencesPage (end), and the wizard if it grows a delete path (not currently present).

Helpers

Two small pure-logic helpers, co-located in ui/src/pages/Alerts/:

  • time-utils.tsformatRelativeTime(iso: string, now?: Date): string returning 2m ago / 1h ago / 3d ago. With a Vitest.
  • severity-utils.tsseverityToAccent(severity: AlertSeverity): DataTableRowAccent mapping CRITICAL→error, MAJOR→warning, MINOR→warning, INFO→info. With a Vitest.

Components touched

File Change
ui/src/pages/Alerts/InboxPage.tsx Rewrite: DataTable + bulk toolbar + EmptyState
ui/src/pages/Alerts/AllAlertsPage.tsx Rewrite: DataTable + ButtonGroup filter + EmptyState
ui/src/pages/Alerts/HistoryPage.tsx Rewrite: DataTable + DateRangePicker + EmptyState
ui/src/pages/Alerts/RulesListPage.tsx Table → DataTable; select → Dropdown; confirm → ConfirmDialog
ui/src/pages/Alerts/SilencesPage.tsx Table → DataTable; FormField grid; confirm → ConfirmDialog
ui/src/pages/Alerts/AlertRow.tsx Delete
ui/src/pages/Alerts/alert-expanded.tsx New — shared expandedContent renderer
ui/src/pages/Alerts/time-utils.ts New
ui/src/pages/Alerts/time-utils.test.ts New
ui/src/pages/Alerts/severity-utils.ts New
ui/src/pages/Alerts/severity-utils.test.ts New
ui/src/pages/Alerts/alerts-page.module.css Slim down to layout-only, DS tokens
ui/src/pages/Alerts/RuleEditor/wizard.module.css Replace legacy tokens → DS tokens
ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx Promote banner / warnings → DS Alert; step body → section-card wrap

Testing

  1. Unit (Vitest):
    • time-utils.test.ts — relative time formatting at 0s / 30s / 5m / 2h / 3d / 10d boundaries.
    • severity-utils.test.ts — all four severities map correctly.
  2. Component tests:
    • Existing AlertStateChip.test.tsx, SeverityBadge.test.tsx keep passing (no change).
    • NotificationBell.test.tsx — unchanged (this component already uses DS correctly per audit).
  3. E2E (Playwright):
    • Inbox empty state renders.
    • AllAlerts filter ButtonGroup switches active state and requery fires.
    • Rules list delete opens ConfirmDialog, confirms, row disappears.
    • Wizard promote banner renders as Alert.
  4. Manual smoke:
    • Light + dark theme on all five pages — verify no raw <table> borders bleeding through; all surfaces use DS card shadows.
    • Screenshot comparison pre/post via already-present Playwright config.

Open questions

None — DS v0.1.56 ships every primitive we need (DataTable, EmptyState, Alert, ButtonGroup, Dropdown, ConfirmDialog, DateRangePicker, FormField, MonoText). If a gap surfaces during implementation, flag it as a separate DS-change discussion per user's standing rule.

Out of scope

  • Keyboard shortcuts (j/k nav, e to ack) — future enhancement.
  • Grouping alerts by rule (collapse duplicates) — future enhancement.
  • MustacheEditor visual restyling — separate concern.
  • Replacing native confirm() outside /alerts — project-wide pattern; changing it all requires separate decision.

Migration risk

  • Low. Changes are localized to ui/src/pages/Alerts/ plus one CSS module file for the wizard. No backend, no DS, no router changes.
  • Openapi schema regeneration not required (no controller changes).
  • Existing tests don't exercise feed-row DOM directly (component tests cover badges/chips only), so row-to-table conversion won't break assertions.