6a8f374678
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.
35 KiB
35 KiB
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::before28s 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 thePageHeader"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: 1remtoken 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 globalinput { ... }rule already inapp.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/#f59e0binstead of--color-mint/coral/sky/citrus. ConfirmModal uses a rawrgba(239, 68, 68, 0.3)glow. Actions page uses#059669for 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
IconGridSelectsort 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
/setuplands you on/with0 providersand 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/routes/+layout.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(lines 132–147) - State:
subpage-hero__titleis 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 (emwords 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.glass,frontend/src/lib/components/Card.svelte,frontend/src/lib/components/PageHeader.svelte.subpage-hero,frontend/src/routes/+page.svelte.hero-card/.panel/.stat-card,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::afterhighlight overlay. Card.svelte even has its own 22px radius next to the global.glass22px radius — they would diverge silently if either gets touched. - Suggestion: Consolidate into one
<GlassPanel>component (or.glass-cardutility) with variantsdefault | hero | panel | cassettefor padding/radius differences. Delete the duplicated::afteroverlays. 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/+page.svelte,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: 1remis 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 withCard.svelte,Button.svelte,Modal.svelte,ConfirmModal.svelte.
F-CONSIST-03 — Hardcoded hex colors bypass theming [HIGH]
- Files:
frontend/src/lib/components/Snackbar.sveltelines 26–31:#059669 / #ef4444 / #3b82f6 / #f59e0bfrontend/src/lib/components/ConfirmModal.svelteline 70:box-shadow: 0 0 16px rgba(239, 68, 68, 0.3)frontend/src/routes/actions/+page.svelteline 379:style="background: {action.enabled ? '#059669' : 'var(--color-muted-foreground)'}"- 25 files in
frontend/src/routes/**contain#xxxliterals
- State: These colors are NOT the Aurora palette —
#059669is 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 rulecolor-no-hexscoped tosrc/**/*.svelteto prevent regression.
F-CONSIST-04 — Form input styling not migrated to Aurora [HIGH]
- Files: 17 routes, ~71 occurrences. Examples:
frontend/src/routes/users/+page.sveltelines 137, 141, 190, 207;frontend/src/routes/providers/+page.sveltelines 303, 309, 323, 333;frontend/src/routes/notification-trackers/TrackerForm.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 globalinput { ... }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 - State: Its
.confirm-btn-canceland.confirm-btn-deletere-implement whatButton variant="secondary"andButton variant="danger"already provide. The danger button even uses rawrgba(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(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-errordesign 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.sveltelines 571–645 - 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 — eventstotalhas 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.sveltelines 1047–1068, 1078–1086 - State: Both the
.hero-titleand.hero-meter-valueare 3rem 500-weight in two different fonts. Side-by-side they create two focal points. - Suggestion: Shrink
.hero-meter-valueto 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.sveltelines 909–927 - 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
chartDayshas 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.sveltelines 498–533, 591–597, 632–658 - State: Active state is conveyed via
.activeclass + a gradient left-bar div. Screen readers cannot announce it. Grep foraria-currentacross 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 - State: The
crumbprop 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 → TemplatesorCommands → 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=emailand/bots?tab=matrixchange the active sidebar item but the<PageHeader>title for those pages is generic ("Targets" / "Bots"). - Suggestion: When
activeTypeis 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
titleas fallback forprefers-reduced-motionusers.
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/users/+page.svelte,frontend/src/routes/targets/TargetForm.svelte - State: Forms rely on HTML5
required/minlengthbrowser validation plus a singleErrorBannershown 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-validclass onIdentityCassette) — generalize it.
F-FORM-02 — Save feedback inconsistent across pages [MEDIUM]
- Files: Settings uses a sticky
SaveBarwith dirty tracking (frontend/src/routes/settings/+page.sveltelines 77–84, 208–214). 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.sveltelines 136–141 + 303;frontend/src/routes/actions/+page.sveltelines 50–56 - State: Once user types in the Name field,
nameManuallyEditedbecomes 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.svelteline 99 - Suggestion: Add
autofocusto 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 - State: Portal mount, focus trap, focus restoration, Escape, Tab cycling,
aria-modal="true",aria-labelledby, body scroll containment viaoverscroll-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,ConfirmModalall do — good.
F-MODAL-02 — Modal backdrop has role="button" [LOW]
- Files:
frontend/src/lib/components/Modal.svelteline 96 - State: The backdrop is a
<div>withrole="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"andaria-labelfrom 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.sveltelines 150–151 - 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.svelteline 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 - 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 - State: 10-line component: dimmed icon + message. No CTA, no illustration, no flavor. The dashboard's inline
.empty-state(lines 1300–1319 of+page.svelte) is richer (has a CTA link) but isn't reused. - Suggestion: Extend
EmptyStateto accept actaslot and atone(with subtle gradient blob behind the icon). On/providersempty: "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
loadErrorstate that renders<Card><ErrorBanner /></Card>but no "Retry" button. - Suggestion:
ErrorBannershould accept anonRetryprop 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.sveltelines 35–63 - 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')}>. Usearia-live="assertive"forsnack.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.csslines 237–241 (globalbutton:focus-visibleoutline 2px primary + offset 2px),frontend/src/routes/+layout.svelteline 894 (.nav-link { border-radius: 12px !important }),frontend/src/routes/+page.sveltelines 1351–1354 (.signal-row--clickable:focus-visible { outline-offset: -2px }). - State: Inverted offset
-2pxmakes 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: 2pxconsistently with abox-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.csslines 484–507,frontend/src/routes/+layout.sveltelines 837–840 - 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: #b6b2d4on--color-glass: rgba(255,255,255,0.04)over the aurora gradient. In the brightest hot-spot of the aurora background (where the#b8a7fflavender peaks),#b6b2d4may fail WCAG AA (4.5:1 for body text). Hasn't been measured. - Suggestion: Run a contrast pass with
--color-muted-foregroundagainst the brightest part of the aurora background. Likely need to bump it to ~#cfcae8for dark theme.
F-A11Y-06 — Toggle switch has no label association [LOW]
- Files:
frontend/src/app.csslines 513–556 - State:
.toggle-switchwraps an<input type="checkbox">and a visual.toggle-track<span>. There's no visible label text oraria-labelrequirement in the global utility. Callers may forget to pass one. - Suggestion: Lift into a
<Toggle>component requiring alabelprop.
9. Responsive design
F-RESP-01 — Sidebar collapse breakpoint is fine; mobile bottom nav covers gracefully [LOW / commendation]
- Files:
frontend/src/routes/+layout.sveltelines 589–668, 1136–1168 - 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 720–880px [LOW]
- Files:
frontend/src/routes/+page.sveltelines 1119–1130 - 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.svelteline 590grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 - State: Between 640–1024px 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.sveltelines 392–410 - State: Secondary text line shows full webhook URL with
break-allwhich 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/+page.svelte - State: After
/setupthe 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.sveltelines 678–682 - State: Topbar shows
⌘K/Ctrl Kchip 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 - 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
/docsroute 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.svelteline 391 - State: Type pill shows raw
provider.typevalue (e.g. "immich", "nextcloud") — not localized. - Suggestion: Use
getDescriptor(type).defaultNameort(\providers.type${PascalName}`)` which exists per project conventions.
F-LOCALE-03 — Mixed Cyrillic glitches in source [LOW]
- Files:
frontend/src/routes/login/+page.svelteline 42 (—instead of em-dash in a comment),frontend/src/routes/users/+page.svelteline 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/onSortin the codebase. Lists are sorted byIconGridSelectwidget (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,actionspages. 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.sveltelines 263–269 - 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:
nfor new,g + pfor "go providers",g + tfor trackers,?to show shortcut sheet. Document in the palette.
Production polish checklist (top 15, prioritized)
- [HIGH] Add
role="status" aria-live="polite"to Snackbar container;assertivefor error toasts. (F-A11Y-01) — one-line fix. - [HIGH] Add
aria-current="page"to every nav link in+layout.svelte. (F-NAV-01, F-A11Y-02) - [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 globalinput { ... }style win. 17 files, ~71 occurrences. (F-CONSIST-04) - [HIGH] Replace hardcoded hex colors (
#059669,#ef4444,#3b82f6,#f59e0b,rgba(239,68,68,...)) with Aurora palette tokens inSnackbar.svelte,ConfirmModal.svelte,actions/+page.svelte, and any remaining sites. (F-CONSIST-03) - [HIGH] First-run onboarding: when
providersCache.items.length === 0, replace dashboard hero with a 4-step "Getting started" checklist. (F-ONBOARD-01) - [HIGH] Consolidate the 5 glass-card abstractions into a single
<GlassPanel variant=...>component; delete redundant::afteroverlays. (F-CONSIST-01) - [HIGH] Introduce a radius scale (
--radius-card / panel / pill / input / chip / tile) and refactorCard.svelte,Button.svelte,Modal.svelte,ConfirmModal.svelteto use it. (F-CONSIST-02) - [MEDIUM] Rewrite
ConfirmModal.svelteto use<Button variant="secondary">and<Button variant="danger">instead of its own buttons. (F-CONSIST-05) - [MEDIUM] Add layout-aware
<Loading shape="hero|grid|list">variants to reduce first-paint layout shift. (F-STATE-01) - [MEDIUM] Extend
<EmptyState>withctaslot and provider-/tracker-/target-specific copy + a contextual CTA. (F-STATE-02) - [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)
- [MEDIUM] Implement column-header sort on
notification-trackers,targets,actions. Persist in cache state. (F-POWER-01) - [MEDIUM] Add multi-select bulk actions (enable/disable, delete) to
targets,notification-trackers,command-trackers. (F-POWER-02) - [MEDIUM] Audit contrast:
--color-muted-foregroundover brightest aurora peak; likely bump dark-theme value from#b6b2d4to ~#cfcae8. (F-A11Y-05) - [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
autofocusto the username input on/login. (F-FORM-05) - Fix
вЂ"/В·Cyrillic encoding glitches inlogin/+page.svelteandusers/+page.svelte. (F-LOCALE-03) - Drop
role="button"from Modal backdrop. (F-MODAL-02) - Replace
provider.typeraw label in provider list rows with localized descriptor name. (F-LOCALE-02) - Add inline empty-state copy to
EventChartwhen allchartDaysvalues 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-motionhonored 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.
i18nparity at 1577 lines for both locales.
End of review.