Files
notify-bridge/.claude/reviews/ui-ux-review.md
T
alexei.dolgolyov 6a8f374678 feat: observability, per-receiver Telegram options, oversized-video fallback
Operability:
- Correlation IDs end-to-end: shared dispatch_id between log lines and
  EventLog rows (event/watcher/scheduled/deferred/action/HA/command paths)
  and a new X-Request-Id middleware that normalizes inbound ids and binds
  request_id into log context.
- dispatch_summary block merged into EventLog.details: per-target
  success/failure counts plus Telegram media delivered/skipped/failed and
  truncated error lists, so partial outcomes surface in the UI.
- Diagnostic mode: admin can flip one module to DEBUG for a bounded
  window with auto-revert (in-memory only; setup_logging() resets on
  boot, lifespan reverts on shutdown). New /diagnostic-mode endpoints
  plus DiagnosticsCassette UI on the settings page.

Telegram:
- Per-receiver options: disable_notification (silent send) and
  message_thread_id (forum-topic routing), wired through the dispatcher
  via a ContextVar so all four send sites (sendMessage / sendPhoto-Video-
  Document / sendMediaGroup / cache-hit POST) pick them up.
- send_large_videos_as_documents target setting: bypass the 50 MB
  sendVideo cap by falling back to sendDocument for oversized videos.
- sendMediaGroup byte-budget enforcement (TELEGRAM_MAX_GROUP_TOTAL_BYTES,
  45 MB) with per-item fallback on chunk failure so a stale file_id no
  longer silently drops a cached asset.

Tests:
- New: diagnostic_mode, dispatch_summary, request_correlation,
  telegram_media_group_partial, telegram_per_send_options.

Docs:
- .claude/reviews/: six-axis production-readiness review of v0.8.1.
- .claude/docs/functional-review-2026-05-28.md: focused review of
  Telegram/Immich/logging subsystems.
2026-05-28 15:19:31 +03:00

409 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# UI / UX Design Review — Notify Bridge frontend
**Reviewed**: 2026-05-22
**Scope**: SvelteKit frontend at `frontend/`, "Aurora / Glass" aesthetic, en + ru locales.
**Reviewer method**: Read `app.css`, `+layout.svelte`, dashboard, login, setup, providers, targets, users, settings (parent), settings/IdentityCassette, notification-trackers, template-configs, actions, bots, plus shared components (Card, Button, Modal, ConfirmModal, AuthLayout, PageHeader, EmptyState, Loading, Snackbar). Cross-cutting Grep passes for inputs, border-radius, ARIA, sort, hex colors.
---
## Executive summary
- **Aurora design language is real and distinctive.** Newsreader display serif + Geist variable sans + Geist Mono, conic-gradient brand orb, animated radial-gradient aurora background (`body::before` 28s drift), gradient pill chips, glow-pulse dots, and the lavender/orchid/mint/citrus/coral/sky palette together give the product a clear visual identity. This is **not** generic admin-template AI slop — the dashboard hero, signal-stream rows, provider deck, and the `PageHeader` "subpage-hero" pattern all carry intentional character that the user will remember.
- **Consistency is the weakest axis.** Five overlapping card container abstractions (`.hero-card`, `.panel`, `.glass`, `Card.svelte`, settings `.cassette`/`.identity`) re-implement the same frosted-glass recipe with diverging radius (22 / 18 / 14 / 12 px) and padding (1.25/1.4 vs 1.3/1.4 vs 2/2.4 rem). A `--radius: 1rem` token is declared but unused. Pick one card module + one radius scale (e.g. `--radius-card: 22px`, `--radius-input: 12px`, `--radius-pill: 999px`).
- **Forms have not been migrated to Aurora.** ~71 occurrences across 17 files still use the legacy raw class string `border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]` instead of the global `input { ... }` rule already in `app.css` (which uses `--color-input-bg`, `--color-rule-strong`, 0.625rem radius, glow focus ring). Result: rounded-md (6px) fields next to rounded-2xl (22px) cards, solid opaque backgrounds inside frosted-glass cards. Removing the override class would auto-restyle every form to match. **HIGH** priority, mostly mechanical.
- **Hardcoded hex colors leak through.** Snackbar uses `#059669` / `#ef4444` / `#3b82f6` / `#f59e0b` instead of `--color-mint/coral/sky/citrus`. ConfirmModal uses a raw `rgba(239, 68, 68, 0.3)` glow. Actions page uses `#059669` for the enabled dot. All bypass theming — they will look wrong in light theme.
- **Snackbar is invisible to screen readers.** No `role="status"` / `aria-live="polite"` / `aria-live="assertive"` on the toast container. Critical confirmations (saved, deleted, error) are never announced. **HIGH** accessibility fix, one-line.
- **No `aria-current="page"` anywhere in the nav** — active state is conveyed only visually (border-radius bar + glow). Active state has no accessible name.
- **No sortable columns, no multi-select bulk actions, anywhere in the app.** Lists rely entirely on `IconGridSelect` sort widgets (newest / oldest, etc.) and per-row icon buttons. For a notification routing system that may accumulate dozens of trackers / targets / configs, this scales poorly.
- **Localization parity is solid string-for-string** (en.json = ru.json = 1577 lines). Russian renders the same characters but several places (hero title, brand row with provider name, stat-card label/value flex) have no length-guard for the longer Russian translations — visible truncation/wrapping likely.
- **Onboarding is a single screen.** After `/setup` lands you on `/` with `0 providers` and a hero saying "all clear" — the most important first-run moment shows nothing to do. No checklist, no empty-dashboard CTA panel, no tour.
- **Power-user feature standout**: ⌘K SearchPalette is present and wired through the topbar, global provider filter, and reduced-motion media-query support. These three deserve credit and should be more discoverable (no in-app hint they exist).
---
## Findings by area
### 1. Design quality vs generic AI aesthetic
#### F-DESIGN-01 — Aurora identity is strong and self-consistent at the macro level [LOW / commendation]
- **Files**: [`frontend/src/app.css`](frontend/src/app.css), [`frontend/src/routes/+layout.svelte`](frontend/src/routes/+layout.svelte), [`frontend/src/routes/+page.svelte`](frontend/src/routes/+page.svelte)
- **State**: Newsreader display serif italic with linear-gradient text-clip is used in hero titles, panel titles, modal titles. Conic brand orb is unique. Aurora drift on body::before is a 28s slow loop that's never busy. The "signal" / "wires" / "on watch" / "pulse" / "stream" / "compose" semantic naming on the dashboard is editorial, not generic admin copy.
- **Verdict**: Keep all of this. Lean *further* into it on the subpages — most list pages currently default back to plain "PageHeader + Card list" without inheriting the dashboard's editorial flavor.
#### F-DESIGN-02 — Italic-serif emphasis loses impact on smaller subpage titles [LOW]
- **Files**: [`frontend/src/lib/components/PageHeader.svelte`](frontend/src/lib/components/PageHeader.svelte) (lines 132147)
- **State**: `subpage-hero__title` is 2.15rem with italic emphasis on a gradient. At that size the gradient italic word is legible but loses the editorial drama it has at the 3rem dashboard hero. Russian translations (`em` words like *«операторы»*) sometimes look cramped because letter-spacing -0.025em is shared with the much larger dashboard hero.
- **Suggestion**: Use a separate letter-spacing scale per font size step, or drop italic emphasis on titles below ~2rem and use color-only emphasis there.
---
### 2. Visual consistency
#### F-CONSIST-01 — Five overlapping card abstractions [HIGH]
- **Files**: [`frontend/src/app.css`](frontend/src/app.css) `.glass`, [`frontend/src/lib/components/Card.svelte`](frontend/src/lib/components/Card.svelte), [`frontend/src/lib/components/PageHeader.svelte`](frontend/src/lib/components/PageHeader.svelte) `.subpage-hero`, [`frontend/src/routes/+page.svelte`](frontend/src/routes/+page.svelte) `.hero-card` / `.panel` / `.stat-card`, [`frontend/src/routes/settings/IdentityCassette.svelte`](frontend/src/routes/settings/IdentityCassette.svelte) `.identity` + `.glass`
- **State**: Six places re-declare the same recipe: `background: var(--color-glass); backdrop-filter: blur(28px) saturate(160%); border: 1px solid var(--color-border); border-radius: 22px; box-shadow: var(--shadow-card);` followed by an `::after` highlight overlay. Card.svelte even has its own 22px radius next to the global `.glass` 22px radius — they would diverge silently if either gets touched.
- **Suggestion**: Consolidate into one `<GlassPanel>` component (or `.glass-card` utility) with variants `default | hero | panel | cassette` for padding/radius differences. Delete the duplicated `::after` overlays. The pattern is good — it's just *copy-pasted* 5+ times.
#### F-CONSIST-02 — Border-radius drift, no scale [HIGH]
- **Files**: [`frontend/src/routes/+layout.svelte`](frontend/src/routes/+layout.svelte), [`frontend/src/routes/+page.svelte`](frontend/src/routes/+page.svelte), [`frontend/src/app.css`](frontend/src/app.css)
- **State**: Radii used: 22, 18, 14, 12, 11, 10, 9, 8, 7, 6, 3, 2 px + 0.3, 0.5, 0.625, 0.85, 1 rem + 9999px. `--radius: 1rem` is declared in the theme but only re-declared — no component reads it.
- **Suggestion**: Define and *use* `--radius-card: 22px; --radius-panel: 18px; --radius-pill: 999px; --radius-input: 12px; --radius-chip: 8px; --radius-tile: 6px;`. Refactor in passes — start with `Card.svelte`, `Button.svelte`, `Modal.svelte`, `ConfirmModal.svelte`.
#### F-CONSIST-03 — Hardcoded hex colors bypass theming [HIGH]
- **Files**:
- [`frontend/src/lib/components/Snackbar.svelte`](frontend/src/lib/components/Snackbar.svelte) lines 2631: `#059669 / #ef4444 / #3b82f6 / #f59e0b`
- [`frontend/src/lib/components/ConfirmModal.svelte`](frontend/src/lib/components/ConfirmModal.svelte) line 70: `box-shadow: 0 0 16px rgba(239, 68, 68, 0.3)`
- [`frontend/src/routes/actions/+page.svelte`](frontend/src/routes/actions/+page.svelte) line 379: `style="background: {action.enabled ? '#059669' : 'var(--color-muted-foreground)'}"`
- 25 files in `frontend/src/routes/**` contain `#xxx` literals
- **State**: These colors are NOT the Aurora palette — `#059669` is emerald-600, our mint is `#7ee8c4`. In light theme the user sees green-on-green that wasn't intended.
- **Suggestion**: Replace all status hexes with `--color-mint/coral/sky/citrus/orchid`. Add a stylelint rule `color-no-hex` scoped to `src/**/*.svelte` to prevent regression.
#### F-CONSIST-04 — Form input styling not migrated to Aurora [HIGH]
- **Files**: 17 routes, ~71 occurrences. Examples: [`frontend/src/routes/users/+page.svelte`](frontend/src/routes/users/+page.svelte) lines 137, 141, 190, 207; [`frontend/src/routes/providers/+page.svelte`](frontend/src/routes/providers/+page.svelte) lines 303, 309, 323, 333; [`frontend/src/routes/notification-trackers/TrackerForm.svelte`](frontend/src/routes/notification-trackers/TrackerForm.svelte); [`frontend/src/routes/targets/TargetForm.svelte`](frontend/src/routes/targets/TargetForm.svelte).
- **State**: `class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]"` is repeated 71+ times. This overrides the global `input { ... }` rule that *already* uses Aurora glass styling.
- **Suggestion**: Delete the class string in all these places. The global rule kicks in and forms instantly look correct. Cross-check that `Tailwind`'s preflight isn't interfering. Spot-check one page (e.g. `users/+page.svelte`), confirm visually, then mass-delete via Grep/Edit.
#### F-CONSIST-05 — ConfirmModal duplicates Button.svelte logic [MEDIUM]
- **Files**: [`frontend/src/lib/components/ConfirmModal.svelte`](frontend/src/lib/components/ConfirmModal.svelte)
- **State**: Its `.confirm-btn-cancel` and `.confirm-btn-delete` re-implement what `Button variant="secondary"` and `Button variant="danger"` already provide. The danger button even uses raw `rgba(239,68,68,...)` instead of `--color-error-fg`.
- **Suggestion**: `<Button variant="secondary" onclick={oncancel}>{cancel}</Button>` and `<Button variant="danger" onclick={onconfirm}>{confirm}</Button>`. Removes ~35 lines of CSS.
#### F-CONSIST-06 — AuthLayout uses a different glass recipe [MEDIUM]
- **Files**: [`frontend/src/lib/components/AuthLayout.svelte`](frontend/src/lib/components/AuthLayout.svelte) (line 68 `.auth-card`)
- **State**: `border-radius: 1rem`, `padding: 2rem`, `backdrop-filter: blur(8px)` (vs the 28px elsewhere), plus its own auth-bg gradient mesh + 32px-grid background that nothing else in the app uses. Has its own `.auth-input` / `.auth-submit` / `.auth-label` / `.auth-error` design language.
- **State pt 2**: Login/setup ends up looking *more* like generic SaaS than the dashboard does. The brand orb from the sidebar isn't on the login screen — instead a small lavender mdi-lan icon in a square.
- **Suggestion**: Reuse the conic brand orb. Use the same glass recipe (28px blur, 22px radius) for `.auth-card`. Either drop the dot-grid `.auth-grid` (it reads as a generic "futuristic SaaS" template) or use it as a deliberate flair on the dashboard hero too.
---
### 3. Information hierarchy
#### F-HIER-01 — Stat cards do triple duty (KPI + nav link + filter context) without ranking [MEDIUM]
- **Files**: [`frontend/src/routes/+page.svelte`](frontend/src/routes/+page.svelte) lines 571645
- **State**: All four stat cards have the same visual weight, same accent intensity (`STAT_ACCENTS[idx]`), and rotate accents by index. When the global provider filter is active the first stat card morphs into a "literal value" card showing provider name (1rem font, very different visual). The accent rotation creates a rainbow row that doesn't carry meaning — events `total` has no semantic reason to be orchid vs. providers being lavender.
- **Suggestion**: Tie accent color to entity type (providers=primary, trackers=mint, targets=sky, throughput=citrus) so the same accent recurs throughout the app for the same concept. Keep the morph behavior but design a distinct "filtered context" stat-card variant — a smaller, narrower chip — so it doesn't compete visually.
#### F-HIER-02 — Hero title and meter compete for attention at desktop width [LOW]
- **Files**: [`frontend/src/routes/+page.svelte`](frontend/src/routes/+page.svelte) lines 10471068, 10781086
- **State**: Both the `.hero-title` and `.hero-meter-value` are 3rem 500-weight in two different fonts. Side-by-side they create two focal points.
- **Suggestion**: Shrink `.hero-meter-value` to 2.4rem and use it as a *secondary* read; let the editorial title be the single dominant element.
#### F-HIER-03 — Pulse chart panel rarely meaningful on first launch [LOW]
- **Files**: [`frontend/src/routes/+page.svelte`](frontend/src/routes/+page.svelte) lines 909927
- **State**: On a fresh install the chart is an empty 0-events grid taking 250-400px vertical space. No empty-state copy inside `EventChart`.
- **Suggestion**: When `chartDays` has all-zero values, replace with a small "No events recorded in the last 30 days — once a tracker fires, the pulse will appear here" inline empty state.
---
### 4. Navigation & wayfinding
#### F-NAV-01 — No `aria-current="page"` on active nav links [HIGH a11y]
- **Files**: [`frontend/src/routes/+layout.svelte`](frontend/src/routes/+layout.svelte) lines 498533, 591597, 632658
- **State**: Active state is conveyed via `.active` class + a gradient left-bar div. Screen readers cannot announce it. Grep for `aria-current` across the whole frontend: zero matches.
- **Suggestion**: Add `aria-current={isActive(child.href) ? 'page' : undefined}` to every nav `<a>`.
#### F-NAV-02 — No breadcrumb on subpages [MEDIUM]
- **Files**: [`frontend/src/lib/components/PageHeader.svelte`](frontend/src/lib/components/PageHeader.svelte)
- **State**: The `crumb` prop only renders a single mono-uppercase tag (e.g. "ROUTING · AUTOMATION") — it's decorative, not navigational. There's no actual breadcrumb chain. For `/template-configs`, `/command-template-configs`, `/tracking-configs`, `/command-configs`, etc., a user landing via deep link has no parent-link to return to.
- **Suggestion**: Make the crumb a real breadcrumb (≤3 levels: `Notifications → Templates` or `Commands → Configs`). Render the prior level as a clickable `<a>`.
#### F-NAV-03 — Deep linking via `?type=<targetType>` and `?tab=<botType>` doesn't update page title [LOW]
- **State**: `/targets?type=email` and `/bots?tab=matrix` change the active sidebar item but the `<PageHeader>` title for those pages is generic ("Targets" / "Bots").
- **Suggestion**: When `activeType` is set, derive the title from it: "Email targets" / "Matrix bots". Improves browser tab titles and the in-page title.
#### F-NAV-04 — Collapsed sidebar tooltip wraps for long Russian translations [LOW]
- **State**: Tooltips for collapsed sidebar nav items use the browser-native `title=` attribute, which gives no glass-style chip. They will use the OS tooltip styling, which clashes with the Aurora aesthetic and clips long ru labels.
- **Suggestion**: Build a small custom tooltip component (or use existing portal helper) for collapsed-sidebar nav. Keep `title` as fallback for `prefers-reduced-motion` users.
---
### 5. Form UX
#### F-FORM-01 — No inline field-level validation, only post-submit error banners [MEDIUM]
- **Files**: [`frontend/src/routes/providers/+page.svelte`](frontend/src/routes/providers/+page.svelte), [`frontend/src/routes/users/+page.svelte`](frontend/src/routes/users/+page.svelte), [`frontend/src/routes/targets/TargetForm.svelte`](frontend/src/routes/targets/TargetForm.svelte)
- **State**: Forms rely on HTML5 `required` / `minlength` browser validation plus a single `ErrorBanner` shown after submit failure. Native browser validation tooltips are pale and don't match Aurora.
- **Suggestion**: Add a per-field `<FieldError>` slot below labels for inline validation (URL syntax, email format, port range). The settings page already has a nice pattern (`url-field-valid` class on `IdentityCassette`) — generalize it.
#### F-FORM-02 — Save feedback inconsistent across pages [MEDIUM]
- **Files**: Settings uses a sticky `SaveBar` with dirty tracking ([`frontend/src/routes/settings/+page.svelte`](frontend/src/routes/settings/+page.svelte) lines 7784, 208214). Most other forms have inline Save buttons inside the card. Some show snackbar success ("snack.userCreated"), some don't.
- **Suggestion**: Standardize: (a) inline "Save" inside the card *plus* (b) snackbar success message *plus* (c) optional sticky SaveBar for multi-field admin forms. Document the pattern in `.claude/docs/frontend-architecture.md`.
#### F-FORM-03 — Forms auto-name from descriptor but offer no way to unlock it back to auto-name [LOW]
- **Files**: [`frontend/src/routes/providers/+page.svelte`](frontend/src/routes/providers/+page.svelte) lines 136141 + 303; [`frontend/src/routes/actions/+page.svelte`](frontend/src/routes/actions/+page.svelte) lines 5056
- **State**: Once user types in the Name field, `nameManuallyEdited` becomes true and the auto-fill stops permanently — no way to ask "go back to default name".
- **Suggestion**: Add a tiny "↺ reset" link next to the name input when `nameManuallyEdited && form.name !== descriptor.defaultName`.
#### F-FORM-04 — No optimistic UI; rows disappear / appear only after server roundtrip [LOW]
- **State**: After delete/create, pages refetch via `cache.fetch(true)`. Visible 200-400ms blank state.
- **Suggestion**: Optimistic insert/remove in the cache stores, with snackbar undo for destructive ops.
#### F-FORM-05 — Login form omits `autofocus` on username [LOW]
- **Files**: [`frontend/src/routes/login/+page.svelte`](frontend/src/routes/login/+page.svelte) line 99
- **Suggestion**: Add `autofocus` to the username input. Saves one keystroke on every login.
---
### 6. Modals & overlays
#### F-MODAL-01 — Modal.svelte is well-built [LOW / commendation]
- **Files**: [`frontend/src/lib/components/Modal.svelte`](frontend/src/lib/components/Modal.svelte)
- **State**: Portal mount, focus trap, focus restoration, Escape, Tab cycling, `aria-modal="true"`, `aria-labelledby`, body scroll containment via `overscroll-behavior: contain`, transition (250ms in/out), 80vh max-height. This is the strongest single component in the codebase.
- **Verdict**: Reuse as the foundation for every overlay. Currently `BlockedByModal`, `EventDetailModal`, `SharedLinkModal`, `ConfirmModal` all do — good.
#### F-MODAL-02 — Modal backdrop has `role="button"` [LOW]
- **Files**: [`frontend/src/lib/components/Modal.svelte`](frontend/src/lib/components/Modal.svelte) line 96
- **State**: The backdrop is a `<div>` with `role="button"`, `tabindex="-1"`, and an onclick to close. That's a common pattern to silence Svelte's a11y warnings, but a screen reader announces "Close, button" twice (once for backdrop, once for the explicit X button).
- **Suggestion**: Drop `role="button"` and `aria-label` from the backdrop; the explicit Close button is enough. Or use `<button class="modal-backdrop">` instead of a div.
#### F-MODAL-03 — Modal panel uses solid `#131520` instead of glass [LOW]
- **Files**: [`frontend/src/lib/components/Modal.svelte`](frontend/src/lib/components/Modal.svelte) lines 150151
- **State**: `--modal-solid-bg: #131520;` is a deliberate choice (probably for readability) but it breaks visual consistency with the rest of the app. The Aurora drift behind it is invisible.
- **Suggestion**: Use `var(--color-glass-elev)` over the blurred backdrop. Or, if the solid choice was deliberate, document why so the next developer doesn't "fix" it.
#### F-MODAL-04 — Confirm-modal "delete" hover uses raw rgba [MEDIUM]
- **Files**: [`frontend/src/lib/components/ConfirmModal.svelte`](frontend/src/lib/components/ConfirmModal.svelte) line 70
- **State**: `box-shadow: 0 0 16px rgba(239, 68, 68, 0.3);` — not themed.
- **Suggestion**: Use `box-shadow: 0 0 16px color-mix(in srgb, var(--color-coral) 40%, transparent);`.
---
### 7. Empty / loading / error states
#### F-STATE-01 — `Loading.svelte` is a single shimmer pattern [MEDIUM]
- **Files**: [`frontend/src/lib/components/Loading.svelte`](frontend/src/lib/components/Loading.svelte)
- **State**: Three or four 4rem shimmer bars. Used as `<Loading />` on virtually every page including hero pages. Doesn't match the actual layout the user will see — looks like a row list even on settings.
- **Suggestion**: Add layout-aware variants: `<Loading shape="hero" />`, `<Loading shape="grid" cols={4} />`, `<Loading shape="list" rows={5} />`. Reduces layout shift on first paint.
#### F-STATE-02 — `EmptyState.svelte` is plain and undifferentiated [MEDIUM]
- **Files**: [`frontend/src/lib/components/EmptyState.svelte`](frontend/src/lib/components/EmptyState.svelte)
- **State**: 10-line component: dimmed icon + message. No CTA, no illustration, no flavor. The dashboard's inline `.empty-state` (lines 13001319 of `+page.svelte`) is richer (has a CTA link) but isn't reused.
- **Suggestion**: Extend `EmptyState` to accept a `cta` slot and a `tone` (with subtle gradient blob behind the icon). On `/providers` empty: "No providers yet — connect Immich, Nextcloud, or Home Assistant to start tracking events" with an "+ Add provider" CTA.
#### F-STATE-03 — Many list pages have no error-recovery action [MEDIUM]
- **Files**: Throughout — most pages have a `loadError` state that renders `<Card><ErrorBanner /></Card>` but no "Retry" button.
- **Suggestion**: `ErrorBanner` should accept an `onRetry` prop and surface a retry button. Standardize across pages.
#### F-STATE-04 — `EventChart` no empty state [LOW]
- See F-HIER-03.
---
### 8. Accessibility
#### F-A11Y-01 — Snackbar has no aria-live [HIGH]
- **Files**: [`frontend/src/lib/components/Snackbar.svelte`](frontend/src/lib/components/Snackbar.svelte) lines 3563
- **State**: Snack container is a plain `<div use:portal>`. Success / error toasts never reach screen readers. Three other files have proper aria-live; this critical one doesn't.
- **Fix**: `<div use:portal class="snackbar-container" role="region" aria-live="polite" aria-label={t('snackbar.region')}>`. Use `aria-live="assertive"` for `snack.type === 'error'`.
#### F-A11Y-02 — No `aria-current="page"` on nav links [HIGH]
- See F-NAV-01.
#### F-A11Y-03 — Custom focus outlines partially overridden [MEDIUM]
- **Files**: [`frontend/src/app.css`](frontend/src/app.css) lines 237241 (global `button:focus-visible` outline 2px primary + offset 2px), [`frontend/src/routes/+layout.svelte`](frontend/src/routes/+layout.svelte) line 894 (`.nav-link { border-radius: 12px !important }`), [`frontend/src/routes/+page.svelte`](frontend/src/routes/+page.svelte) lines 13511354 (`.signal-row--clickable:focus-visible { outline-offset: -2px }`).
- **State**: Inverted offset `-2px` makes the focus ring sit *inside* the row, which against the glass-strong hover-bg ends up nearly invisible at certain accent positions.
- **Suggestion**: Use `outline-offset: 2px` consistently with a `box-shadow: 0 0 0 2px var(--color-glass)` ringer if needed for contrast.
#### F-A11Y-04 — `prefers-reduced-motion` is honored — commendation [LOW]
- **Files**: [`frontend/src/app.css`](frontend/src/app.css) lines 484507, [`frontend/src/routes/+layout.svelte`](frontend/src/routes/+layout.svelte) lines 837840
- **State**: Aurora drift, brand-version pulse, stagger entrances, signal-row hover transitions, paginator transitions all gated. Smooth scroll override too. Solid implementation.
#### F-A11Y-05 — Color contrast risk on glass surfaces [MEDIUM]
- **State**: `--color-muted-foreground: #b6b2d4` on `--color-glass: rgba(255,255,255,0.04)` over the aurora gradient. In the brightest hot-spot of the aurora background (where the `#b8a7ff` lavender peaks), `#b6b2d4` may fail WCAG AA (4.5:1 for body text). Hasn't been measured.
- **Suggestion**: Run a contrast pass with `--color-muted-foreground` against the brightest part of the aurora background. Likely need to bump it to ~`#cfcae8` for dark theme.
#### F-A11Y-06 — Toggle switch has no label association [LOW]
- **Files**: [`frontend/src/app.css`](frontend/src/app.css) lines 513556
- **State**: `.toggle-switch` wraps an `<input type="checkbox">` and a visual `.toggle-track` `<span>`. There's no visible label text or `aria-label` requirement in the global utility. Callers may forget to pass one.
- **Suggestion**: Lift into a `<Toggle>` component requiring a `label` prop.
---
### 9. Responsive design
#### F-RESP-01 — Sidebar collapse breakpoint is fine; mobile bottom nav covers gracefully [LOW / commendation]
- **Files**: [`frontend/src/routes/+layout.svelte`](frontend/src/routes/+layout.svelte) lines 589668, 11361168
- **State**: Below 767px the desktop sidebar hides and mobile bottom-nav appears with primary 4 keys + search + more. Mobile "More" panel mirrors the full desktop tree. Solid.
#### F-RESP-02 — Hero meter wraps awkwardly between 720880px [LOW]
- **Files**: [`frontend/src/routes/+page.svelte`](frontend/src/routes/+page.svelte) lines 11191130
- **State**: Below 880px the hero collapses to one column, but the meter row pills wrap to a third row on Russian translations of "providers/targets/armed".
- **Suggestion**: Add an intermediate breakpoint (`max-width: 1024px`) where pill labels switch from `"5 providers"` to a tooltip-only count.
#### F-RESP-03 — Stat-card grid drops to 1 column at sm: [MEDIUM]
- **Files**: [`frontend/src/routes/+page.svelte`](frontend/src/routes/+page.svelte) line 590 `grid-cols-1 sm:grid-cols-2 lg:grid-cols-4`
- **State**: Between 6401024px stat cards are 2-wide. At tablet sizes the cards become huge and dilute the dashboard density.
- **Suggestion**: Cap stat-card max-width at ~300px or switch to `auto-fit, minmax(200px, 1fr)` so they don't grow uncontrollably.
#### F-RESP-04 — List rows don't gracefully truncate webhook URLs on mobile [LOW]
- **Files**: [`frontend/src/routes/providers/+page.svelte`](frontend/src/routes/providers/+page.svelte) lines 392410
- **State**: Secondary text line shows full webhook URL with `break-all` which on very narrow viewports gives a 4-line wrap.
- **Suggestion**: Use the `shortenUrl()` helper (already defined for the meta-tile path) on the narrow-screen secondary line too.
---
### 10. Onboarding
#### F-ONBOARD-01 — Setup → empty dashboard with no guidance [HIGH]
- **Files**: [`frontend/src/routes/setup/+page.svelte`](frontend/src/routes/setup/+page.svelte), [`frontend/src/routes/+page.svelte`](frontend/src/routes/+page.svelte)
- **State**: After `/setup` the user lands on `/` with 0 providers, hero says *"all clear"* (literally "Nothing to do"). Wasted first impression.
- **Suggestion**: First-run detection (`providersCache.items.length === 0 && targetsCache.items.length === 0`) replaces the dashboard hero with a 3-4 step "Getting started" checklist: (1) Add a provider · (2) Connect a bot · (3) Create a target · (4) Wire your first tracker. Each step is a CTA card. Persist completion to localStorage so it disappears once finished.
#### F-ONBOARD-02 — No in-app discovery of ⌘K palette [MEDIUM]
- **Files**: [`frontend/src/routes/+layout.svelte`](frontend/src/routes/+layout.svelte) lines 678682
- **State**: Topbar shows `⌘K` / `Ctrl K` chip but only that. No "Press ⌘K to jump to any page" hint anywhere.
- **Suggestion**: First-visit toast: "Tip: Press ⌘K from anywhere to search providers, trackers, and pages". Dismissible.
#### F-ONBOARD-03 — Login screen has no help / forgot-password / docs link [LOW]
- **Files**: [`frontend/src/routes/login/+page.svelte`](frontend/src/routes/login/+page.svelte)
- **State**: Plain username + password. For self-hosted users who lost the admin password, there's no link to the recovery docs.
- **Suggestion**: Small "Need help?" link to docs (the `/docs` route exists).
---
### 11. Microcopy
#### F-COPY-01 — Dashboard hero copy is editorial — commendation [LOW]
- "Live · throughput 24h · armed · providers" reads more like a control-room dashboard than CRUD admin. Keep doing this on the rest of the app.
#### F-COPY-02 — Many subpages use literal entity-name copy [MEDIUM]
- E.g. "Add provider" / "Add target" / "Add tracker" / "Add user". Editorial would be "Connect a provider" / "Define a target" / "Wire a tracker" / "Invite a user". Lean into verbs that match the dashboard's "wires / signals / on watch" vocabulary.
#### F-COPY-03 — Russian translations match en line-count but no length QA visible [LOW]
- File sizes match exactly (1577 lines each). That's just structural parity, not visual parity. Russian tends to be 20-30% longer for the same concept; flagged places likely have layout issues (hero title em, stat-card values, sidebar nav labels).
- **Suggestion**: Set up a Playwright snapshot test that switches locale=ru and screenshots dashboard + a representative list page to catch overflow visually.
---
### 12. Localization parity
#### F-LOCALE-01 — "Notify Bridge" wordmark stays in English [LOW / correct]
- Brand. Don't translate.
#### F-LOCALE-02 — Provider type label not localized in list rows [LOW]
- **Files**: [`frontend/src/routes/providers/+page.svelte`](frontend/src/routes/providers/+page.svelte) line 391
- **State**: Type pill shows raw `provider.type` value (e.g. "immich", "nextcloud") — not localized.
- **Suggestion**: Use `getDescriptor(type).defaultName` or `t(\`providers.type${PascalName}\`)` which exists per project conventions.
#### F-LOCALE-03 — Mixed Cyrillic glitches in source [LOW]
- **Files**: [`frontend/src/routes/login/+page.svelte`](frontend/src/routes/login/+page.svelte) line 42 (`—` instead of em-dash in a comment), [`frontend/src/routes/users/+page.svelte`](frontend/src/routes/users/+page.svelte) line 166 (`В·` instead of `·`)
- **State**: Encoding-corrupt characters in source comments and one user-facing dot. Pre-existing — files were probably edited with the wrong encoding at some point.
- **Suggestion**: Grep `вЂ` / `В·` across the repo and fix. Add a pre-commit hook that fails on non-UTF8 chars in `.svelte` / `.ts` / `.json`.
---
### 13. Power-user features
#### F-POWER-01 — No sortable columns anywhere [MEDIUM]
- Confirmed by Grep: no `aria-sort` / `sortable` / `onSort` in the codebase. Lists are sorted by `IconGridSelect` widget (newest / oldest / name).
- **Suggestion**: For long lists (trackers, targets), add column-header sort affordance. Even minimal: clicking the "Name" or "Provider" header re-sorts. Use cache state so sort persists across nav.
#### F-POWER-02 — No multi-select bulk actions [MEDIUM]
- Grep for `bulkAction` / `selectAll`: only the locale files contain those strings (likely as i18n keys that are never used). No checkbox UI.
- **Suggestion**: Add a checkbox column on `targets`, `notification-trackers`, `command-trackers`, `actions` pages. Bulk-enable / bulk-delete are the obvious ones.
#### F-POWER-03 — ⌘K palette is the strongest power feature, under-promoted [MEDIUM]
- See F-ONBOARD-02.
#### F-POWER-04 — Sidebar group expand/collapse is persisted but no "expand all / collapse all" [LOW]
- **Files**: [`frontend/src/routes/+layout.svelte`](frontend/src/routes/+layout.svelte) lines 263269
- **Suggestion**: Add a right-click menu on a group header, or a tiny "collapse all" icon at the bottom of the nav rail.
#### F-POWER-05 — No keyboard shortcuts beyond ⌘K [LOW]
- **Suggestion**: `n` for new, `g + p` for "go providers", `g + t` for trackers, `?` to show shortcut sheet. Document in the palette.
---
## Production polish checklist (top 15, prioritized)
1. **[HIGH]** Add `role="status" aria-live="polite"` to Snackbar container; `assertive` for error toasts. (F-A11Y-01) — one-line fix.
2. **[HIGH]** Add `aria-current="page"` to every nav link in `+layout.svelte`. (F-NAV-01, F-A11Y-02)
3. **[HIGH]** Mass-replace the legacy form-input class (`border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]`) with nothing — let the global `input { ... }` style win. 17 files, ~71 occurrences. (F-CONSIST-04)
4. **[HIGH]** Replace hardcoded hex colors (`#059669`, `#ef4444`, `#3b82f6`, `#f59e0b`, `rgba(239,68,68,...)`) with Aurora palette tokens in `Snackbar.svelte`, `ConfirmModal.svelte`, `actions/+page.svelte`, and any remaining sites. (F-CONSIST-03)
5. **[HIGH]** First-run onboarding: when `providersCache.items.length === 0`, replace dashboard hero with a 4-step "Getting started" checklist. (F-ONBOARD-01)
6. **[HIGH]** Consolidate the 5 glass-card abstractions into a single `<GlassPanel variant=...>` component; delete redundant `::after` overlays. (F-CONSIST-01)
7. **[HIGH]** Introduce a radius scale (`--radius-card / panel / pill / input / chip / tile`) and refactor `Card.svelte`, `Button.svelte`, `Modal.svelte`, `ConfirmModal.svelte` to use it. (F-CONSIST-02)
8. **[MEDIUM]** Rewrite `ConfirmModal.svelte` to use `<Button variant="secondary">` and `<Button variant="danger">` instead of its own buttons. (F-CONSIST-05)
9. **[MEDIUM]** Add layout-aware `<Loading shape="hero|grid|list">` variants to reduce first-paint layout shift. (F-STATE-01)
10. **[MEDIUM]** Extend `<EmptyState>` with `cta` slot and provider-/tracker-/target-specific copy + a contextual CTA. (F-STATE-02)
11. **[MEDIUM]** Visual length-QA pass for Russian — at least dashboard hero, providers list, settings hero, stat-cards. Playwright screenshot test. (F-COPY-03, F-LOCALE-02)
12. **[MEDIUM]** Implement column-header sort on `notification-trackers`, `targets`, `actions`. Persist in cache state. (F-POWER-01)
13. **[MEDIUM]** Add multi-select bulk actions (enable/disable, delete) to `targets`, `notification-trackers`, `command-trackers`. (F-POWER-02)
14. **[MEDIUM]** Audit contrast: `--color-muted-foreground` over brightest aurora peak; likely bump dark-theme value from `#b6b2d4` to ~`#cfcae8`. (F-A11Y-05)
15. **[MEDIUM]** Replace inline browser-native `title=` tooltips on the collapsed sidebar with a custom Aurora-styled tooltip (using the existing portal helper). (F-NAV-04)
### Quick wins (bonus, under an hour each)
- Add `autofocus` to the username input on `/login`. (F-FORM-05)
- Fix `вЂ"` / `В·` Cyrillic encoding glitches in `login/+page.svelte` and `users/+page.svelte`. (F-LOCALE-03)
- Drop `role="button"` from Modal backdrop. (F-MODAL-02)
- Replace `provider.type` raw label in provider list rows with localized descriptor name. (F-LOCALE-02)
- Add inline empty-state copy to `EventChart` when all `chartDays` values are 0. (F-HIER-03)
---
## What's working — keep doing it
- The conic-gradient brand orb, animated aurora background, Newsreader italic emphasis, gradient pill chips, glow-pulse dots — distinctive identity.
- `Modal.svelte` (focus trap, restore, portal, escape, scroll containment).
- `prefers-reduced-motion` honored across every animation surface.
- Global ⌘K search palette, global provider filter, persisted sidebar state, persisted nav-group expansion.
- Editorial copy on dashboard (`signal stream`, `on watch`, `pulse`, `wires`, `compose`).
- Snackbar with detail-toggle expansion for error context.
- Mobile "More" panel that mirrors the full desktop nav tree.
- 6-file template-variable sync rule honored by project conventions.
- `i18n` parity at 1577 lines for both locales.
End of review.