diff --git a/CLAUDE.md b/CLAUDE.md index 2aed479..aad376b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -383,6 +383,71 @@ Non-goals for this phase (same discipline as every phase so far): someone with database superuser access; that's an operational control, out of scope here and named explicitly, not silently assumed away. +## What "done" looks like for Phase 5 + +**Status: shipped.** A ground-up frontend redesign — visual direction, +a real design system, navigation/IA, charting, dashboard panels, +query/search, and alerting UI — plus an accessibility pass, all verified +against a live docker-compose stack with real seeded data, not just +`npm run check`/`npm run build` passing. See `/docs/design-system.md` +for the token system and component library, and +`/docs/phase-5-runbook.md` for the full verification log, including five +real bugs this phase's live-verification discipline caught that a +type-checked, successfully-building frontend would not have surfaced on +its own. + +The visual direction ("Signal": near-neutral grayscale UI, color rationed +to the four-tier severity system plus a single interactive accent, real +dark-mode-as-default) was picked from three proposed directions before +any token or component work started, per an explicit stop point in this +phase's brief. The charting library (ECharts, over Observable Plot and +raw D3 — see the design-system doc for the reasoning and the verified +bundle-size/perf numbers) was likewise confirmed before being wired into +every panel type, the second explicit stop point. + +Two of the five bugs this phase's verification caught were backend bugs +with no connection to the frontend redesign itself, only surfaced +because getting real dashboard/alert data to verify the new UI against +required actually exercising write paths nothing had exercised since +Phase 4's `tenant_id` migrations landed: + +- `alerting`'s `rulestore.Create`/`ApplyTransition` never populated the + `tenant_id` column Phase 4 added to `alert_state`/`delivery_log` (with + a `NOT NULL` constraint) — every alert rule created against a + Phase-4-or-later database silently failed. Existing rows all had a + value from Phase 4's backfill migration, which is exactly why this + went uncaught: Phase 4's own verification never created a *new* rule + post-migration, and its runbook already discloses that Docker access + was lost partway through that phase. +- `dashboard_panels`'s `viz_type` CHECK constraint was never updated + alongside `heatmap`'s addition to the Go/TS validators — a three-place + change (Go validator, TS union, DB constraint), not two. + +Both are fixed (`alerting/internal/rulestore/store.go`, +`metadata/migrations/0035_add_heatmap_viz_type.sql`) and confirmed +against a live stack: rule creation → evaluation → firing → a real +(failed, to an intentionally fake webhook) delivery attempt, and a +heatmap panel created, persisted, and rendered end to end. See the +runbook for the other three findings (one more real product bug — a +`findIndex`/nullish-coalescing bug in the chart-pivoting logic that made +every `single_stat` panel render `0` — and two real accessibility +findings caught by axe-core against live-rendered pages with real data, +not fixture data or empty states). + +Non-goals for this phase (same discipline as every phase so far): +- No query-language or data-model changes beyond the one narrowly + justified exception: `heatmap` as a `VizType`, needed to feed a new + visualization, not a new query capability. +- No changes to tenant isolation, RBAC, SSO, or audit logging — Phase 4's + surface area is untouched; this phase is presentation-layer only. +- No mobile-phone-width layout — responsive verification stops at + tablet-landscape width, per the brief's explicit scope ("laptop/ + tablet-landscape," not phone-width). +- No fuzzy search in the command palette, no data-grid virtualization for + very large result sets, no chart types beyond the five built + (time-series, bar, single-stat, heatmap, top-N) — real, disclosed + future work, not oversights. + ## When in doubt Ask before: changing the pinned stack, adding a new external dependency that pulls in a large transitive tree, or making an architectural decision diff --git a/docs/design-system.md b/docs/design-system.md new file mode 100644 index 0000000..a5358cc --- /dev/null +++ b/docs/design-system.md @@ -0,0 +1,371 @@ +# Sentry Design System (Phase 5) + +Direction: **Signal** — cockpit/ICU-monitor instrumentation logic. Color +is a rationed resource: the UI is near-neutral grayscale everywhere, so +the four severity tiers land as genuinely the most saturated colors +anywhere on screen, not one saturated color competing with a dozen +decorative ones. See the Phase 5 design-direction review (three options +were presented; Signal was the one picked) for the two rejected +directions and the full rationale. + +This doc is the source of truth for the token system and component +library `/web/src/lib/components/ui` implements — read it before adding +a new color, spacing value, or component, rather than reaching for a +literal hex or one-off markup the way Phase 0-3's pages did. + +## Status + +**Status: Phase 5 shipped.** All ten tasks are built: the token system, +the `ui/` component library, the persistent sidebar nav + command +palette, a real ECharts-based charting layer (5 chart types, drill-down, +zoom), the dashboard panel rebuild (GridStack drag-and-drop + live-preview +panel editor), the query/search redesign (CodeMirror syntax highlighting ++ autocomplete, sortable/resizable/expandable results), the alerting UI +redesign (severity-state pill + delivery timeline), and an accessibility +pass driven by real axe-core runs against the live app (not static +analysis) — see the Accessibility section below for what that actually +caught. See `/docs/phase-5-runbook.md` for the full verification log. + +Verified in a real browser against a live docker-compose stack with real +seeded data (not just fixtures): dark/light/system theme and +comfortable/compact density switching persisting across navigation and +reload; command palette open/filter/keyboard-navigate/go-to; every nav +route including dashboard/alert detail pages with real panels and real +alert history; all 6 panel viz types rendering real query results; a +firing alert rule's full state-history timeline; keyboard-only operation +of the query editor, results table (sort/expand), and dashboard grid. + +## Fonts + +Self-hosted (not a Google Fonts CDN link — no runtime dependency on a +third party for the app to render correctly): `web/static/fonts/`, +licensed under the SIL Open Font License (see that directory's +`LICENSE.txt`). + +| Role | Typeface | Why | +|---|---|---| +| UI (nav, labels, headings, body) | Overpass | Originally drawn for U.S. highway signage — engineered to be read correctly, fast, under bad conditions. A more honest reason to pick a typeface for an incident-response tool than "it looks modern." | +| Data (log tables, code, query bar, numbers) | Overpass Mono | Same family as the UI face — one typeface end to end removes even the small cognitive cost of a font pairing, matching the direction's overall restraint. | + +Both are variable fonts (one file covers the full weight range), loaded +via `@font-face` in `web/src/lib/styles/fonts.css`. + +## Color tokens + +Defined in `web/src/lib/styles/tokens.css`. Dark is the literal default +— `:root` defines the dark palette directly, light is the override +(both `@media (prefers-color-scheme: light)` for an unset preference and +`[data-theme="light"]` for an explicit one) — not a retrofit where light +is `:root` and dark is bolted on. Component CSS must only ever read a +token, never a literal hex; that's what makes the theme/density toggles +a token swap instead of a per-component rewrite. + +| Token | Dark | Light | Use | +|---|---|---|---| +| `--color-bg` | `#0a0a0b` | `#f7f7f8` | Page background | +| `--color-surface` | `#17181a` | `#ffffff` | Cards, tables, inputs | +| `--color-surface-raised` | `#1e2023` | `#ffffff` | Hover states, popovers | +| `--color-border` / `--color-border-strong` | `#2a2c2f` / `#3a3d41` | `#dfe0e2` / `#c7c9cc` | Dividers, input borders | +| `--color-text` / `--color-text-muted` / `--color-text-faint` | `#f0f0f1` / `#85888d` / `#8a8d92` | `#101113` / `#6b6e73` / `#75787d` | Body text hierarchy | +| `--color-accent` / `--color-accent-strong` | `#3fb6ff` | `#0b84d6` | Interactive elements only — links, primary buttons, focus rings, active nav. Never reused for severity (see below); a semantic color competing with the brand accent defeats the point of "color means something." | + +### Severity tiers + +The schema carries seven OTel severities +(`TRACE`/`DEBUG`/`INFO`/`WARN`/`ERROR`/`FATAL`/`UNSPECIFIED`, see +`/storage/README.md`). Seven colors would be seven things to memorize at +a glance; `web/src/lib/severity.ts`'s `severityTier()` collapses them to +four accent tiers plus one "quiet" state: + +| OTel severity | Tier | Dark | Light | +|---|---|---|---| +| `TRACE`, `DEBUG`, `UNSPECIFIED` | quiet | `#85888d` | `#6b6e73` | +| `INFO` | info | `#4c8dff` | `#1a63d6` | +| `WARN` | warn | `#f5c242` | `#8c6800` | +| `ERROR` | error | `#ff6a39` | `#c94b1e` | +| `FATAL` | critical | `#ff2d78` | `#c21362` | + +Chosen as a blue → amber → orange → magenta progression — hue *and* +lightness both shift at every step, so no two adjacent tiers rely on +red-vs-green to be told apart, and the sequence should survive grayscale +and protanopia/deuteranopia simulation. That reasoning hasn't been +verified with an actual simulator yet — do that before treating it as +confirmed accessible, not just plausible. + +Each tier also has a translucent `-bg` token (e.g. `--color-sev-warn-bg`) +for chip/pill backgrounds. These are tuned independently of the solid +foreground colors above, not derived from them by a fixed formula: a +translucent color tints *toward* its own hue as alpha increases, which +for these saturated, low-luminance hues (critical's magenta especially) +*lowers* contrast against the foreground text the higher the alpha goes +— the opposite of the intuitive "more opaque background is safer" +assumption. `--color-sev-warn` itself was darkened in light mode +(`#9c7300` → `#8c6800`) for the same underlying reason: axe-core caught +the *plain* light-mode warn text failing AA (4.32:1) against white +before any background was even involved. See the Accessibility section. + +Use `` +(`ui/SeverityBadge.svelte`) to render one of these — it owns the OTel +string → tier mapping so call sites can't invent a sixth color by hand. +For non-log-severity status (success/danger/neutral/accent — e.g. a +"saved" confirmation), use `` instead; the two are +deliberately separate components so a form-validation color can never +accidentally collide with a log-severity one. + +## Type scale + +14px base (dense-first — this is a tool for reading log tables, not +marketing copy), 1.2 modular ratio: + +`--text-xs` 11px · `--text-sm` 13px · `--text-base` 14px · `--text-md` +16px · `--text-lg` 20px · `--text-xl` 28px · `--text-2xl` 40px + +`--font-weight-normal` 400, `--font-weight-medium` 600, +`--font-weight-bold` 700. `font-variant-numeric: tabular-nums` is set +globally on `body` so columns of numbers/timestamps align. + +## Spacing, radius, shadow + +8px-based spacing scale: `--space-1` through `--space-8` (4px, 8px, +12px, 16px, 24px, 32px, 48px, 64px). Radius: `--radius-sm` 4px, +`--radius-md` 6px, `--radius-lg` 10px, `--radius-full` (pills). Shadow +is used sparingly, matching Signal's restraint — only real overlays +(modals, the command palette, tooltips) get one (`--shadow-sm/md/lg`); +inline UI never does. + +## Density + +Two presets, one token swap — `html.density-compact` overrides +`--row-height`, `--row-padding-y/x`, `--panel-padding`, `--control-height`, +and drops `--text-base` to `--text-sm`. Comfortable (the default) suits +dashboards and forms; compact suits log tables and query results. Toggle +via `$lib/density.svelte.ts`'s `setDensity()`/`toggleDensity()` — a +global, persisted (`localStorage['sentry.density']`) preference, not a +per-page setting, so switching it on one page carries to the next. +`web/src/app.html` has a synchronous inline script that applies the +stored value before first paint, so there's no flash of the wrong +density on reload; keep that script's storage key/values in sync with +`density.svelte.ts` if either changes. + +## Theme + +`$lib/theme.svelte.ts`, same persisted/synchronous-apply shape as +density. Three states — `'dark' | 'light' | 'system'` — but unlike most +apps, the *unset* default is `'dark'`, not `'system'`. That's the actual +point of "real dark mode as the default, not an afterthought": a +first-time visitor on a light-OS machine still lands in dark. `'system'` +is available as a deliberate opt-in for anyone who wants their OS +setting to win instead. + +## Components (`web/src/lib/components/ui/`) + +Import from the barrel: `import { Button, Input, ... } from +'$lib/components/ui';` + +| Component | Notes | +|---|---| +| `Button` | `variant`: `primary`/`secondary`/`ghost`/`danger`. `size`: `sm`/`md`. Renders `` when `href` is passed, `