feat(notices): add system notice infrastructure

Server-side notice registry with per-user condition evaluation (firstLogin,
existingUserBeforeVersion, addonEnabled, dateWindow, role, custom).
Notices are sorted by priority then severity, filtered against dismissals
stored in a new user_notice_dismissals table, and served via
GET /api/system-notices/active + POST /api/system-notices/:id/dismiss.

Client renders notices through a host component that partitions by
display type (modal / banner / toast). The modal renderer supports
multi-page pagination with directional slide transitions, keyboard
navigation, and correct dismiss-all semantics on CTA / X / ESC.
Dismissals are optimistic with a single background retry.

Includes 3.0.0 upgrade notices (v3-photos, v3-journey, v3-features),
onboarding welcome modal, and full i18n coverage across 15 languages.
The /journey route is addon-gated on both client and server.

Also includes: unit + integration test suites, registry integrity test
that validates action CTA IDs against client source, and technical
documentation in docs/system-notices.md.
This commit is contained in:
jubnl
2026-04-16 14:36:33 +02:00
parent 3b069bc543
commit 293506217e
46 changed files with 3539 additions and 50 deletions
+104
View File
@@ -0,0 +1,104 @@
import type { SystemNotice } from './types.js';
/**
* SYSTEM NOTICE REGISTRY
*
* Rules for authoring:
* - NEVER remove or renumber entries — dismissal tracking is keyed by `id`.
* - `id` must be globally unique and stable across deployments.
* - Title: ≤40 chars, sentence case, no trailing punctuation.
* - Body: markdown (modal) or plain text (banner/toast). ≤400/140/80 chars.
* - CTA label: ≤20 chars, a verb.
* - Never hardcode version numbers/dates in translated strings — use bodyParams.
* - See plans/system-notices/00-overview.md for full authoring guidelines.
*/
export const SYSTEM_NOTICES: SystemNotice[] = [
// ── 3.0.0 upgrade notices (shown as a multipage modal to pre-3.0 users) ─────
{
// Page 1 — breaking change first (warn → sorts before the two info notices)
id: 'v3-photos',
display: 'modal',
severity: 'warn',
icon: 'ImageOff',
titleKey: 'system_notice.v3_photos.title',
bodyKey: 'system_notice.v3_photos.body',
dismissible: true,
conditions: [{ kind: 'existingUserBeforeVersion', version: '3.0.0' }],
publishedAt: '2026-04-16T00:00:00Z',
priority: 90,
},
{
// Page 2 — flagship feature (only when Journey addon is enabled)
id: 'v3-journey',
display: 'modal',
severity: 'info',
icon: 'BookOpen',
titleKey: 'system_notice.v3_journey.title',
bodyKey: 'system_notice.v3_journey.body',
highlights: [
{ labelKey: 'system_notice.v3_journey.highlight_timeline', iconName: 'CalendarDays' },
{ labelKey: 'system_notice.v3_journey.highlight_photos', iconName: 'Images' },
{ labelKey: 'system_notice.v3_journey.highlight_share', iconName: 'Globe' },
{ labelKey: 'system_notice.v3_journey.highlight_export', iconName: 'FileText' },
],
cta: {
kind: 'nav',
labelKey: 'system_notice.v3_journey.cta_label',
href: '/journey',
},
dismissible: true,
conditions: [
{ kind: 'existingUserBeforeVersion', version: '3.0.0' },
{ kind: 'addonEnabled', addonId: 'journey' },
],
publishedAt: '2026-04-16T00:00:00Z',
priority: 80,
},
{
// Page 3 — other highlights
id: 'v3-features',
display: 'modal',
severity: 'info',
icon: 'Sparkles',
titleKey: 'system_notice.v3_features.title',
bodyKey: 'system_notice.v3_features.body',
highlights: [
{ labelKey: 'system_notice.v3_features.highlight_dashboard', iconName: 'LayoutDashboard' },
{ labelKey: 'system_notice.v3_features.highlight_offline', iconName: 'WifiOff' },
{ labelKey: 'system_notice.v3_features.highlight_search', iconName: 'Search' },
{ labelKey: 'system_notice.v3_features.highlight_import', iconName: 'FileInput' },
],
dismissible: true,
conditions: [{ kind: 'existingUserBeforeVersion', version: '3.0.0' }],
publishedAt: '2026-04-16T00:00:00Z',
priority: 70,
},
// ── Onboarding ─────────────────────────────────────────────────────────────
{
id: 'welcome-v1',
display: 'modal',
severity: 'info',
icon: 'Sparkles',
titleKey: 'system_notice.welcome_v1.title',
bodyKey: 'system_notice.welcome_v1.body',
highlights: [
{ labelKey: 'system_notice.welcome_v1.highlight_plan', iconName: 'Map' },
{ labelKey: 'system_notice.welcome_v1.highlight_share', iconName: 'Users' },
{ labelKey: 'system_notice.welcome_v1.highlight_offline', iconName: 'WifiOff' },
],
cta: {
kind: 'action',
labelKey: 'system_notice.welcome_v1.cta_label',
actionId: 'open:trip-create',
},
dismissible: true,
conditions: [{ kind: 'firstLogin' }],
publishedAt: '2026-04-16T00:00:00Z',
priority: 100,
},
];