# 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 `FormField`s 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:

```css
.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.ts` — `formatRelativeTime(iso: string, now?: Date): string` returning `2m ago` / `1h ago` / `3d ago`. With a Vitest.
- `severity-utils.ts` — `severityToAccent(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.
