docs: define milestone v2.0 requirements

This commit is contained in:
lorentz 2026-07-10 12:01:02 -04:00
parent 46a775e81f
commit e0a41da051

View file

@ -1,254 +1,57 @@
# Requirements: Pulse Mobile Shell Redesign
# Requirements: PAX8 Integration (v2.0)
**Defined:** 2026-05-03
**Core Value:** A manager can open Pulse on their phone and, in under 30 seconds, see the state of the business and triage tickets — without ever needing to switch to desktop for read-only awareness.
**Source spec:** `docs/superpowers/specs/2026-05-03-mobile-shell-design.md`
**Defined:** 2026-07-10
**Core Value:** Managers can see PAX8 subscription costs and seat counts alongside existing Autotask company data in Pulse, without manually cross-referencing PAX8's own portal.
**Source:** `.planning/seeds/SEED-002-pax8-integration.md` (scoped via `/gsd-explore` on 2026-07-10)
## v1 Requirements
## v2.0 Requirements
Requirements for this milestone. Each maps to a spec section and a roadmap phase.
Requirements for this milestone. Each maps to a roadmap phase.
### PWA — Progressive Web App scaffolding (spec §4)
### PAX8 — Client & Auth
- [ ] **PWA-01**: `public/manifest.json` exists with name "Pulse", short_name "Pulse", `display: "standalone"`, `start_url: "/mobile"`, theme/background colors matching dark and light shells
- [ ] **PWA-02**: Manifest is referenced from `app/layout.tsx` via `<link rel="manifest">`
- [ ] **PWA-03**: Viewport meta in `app/layout.tsx` includes `viewport-fit=cover`
- [ ] **PWA-04**: Header and bottom tab bar respect `env(safe-area-inset-top)` and `env(safe-area-inset-bottom)` (Tailwind arbitrary values or shared utility class)
- [ ] **PAX8-01**: Pulse authenticates to the PAX8 REST API (`api.pax8.com/v1`) via OAuth2 client-credentials, using the developer-provisioned client ID/secret
- [ ] **PAX8-02**: `isPax8Configured()` helper reports whether PAX8 credentials are present, following the existing `is<Name>Configured()` factory pattern (`lib/services/pax8-factory.ts`)
### Shell — `/mobile` layout (spec §5)
### PAX8 — Data Sync
- [x] **SHELL-01**: New `app/mobile/layout.tsx` replaces the current layout (rebuild in place — no parallel `/mobile-v2`)
- [ ] **SHELL-02**: Sticky top header: `bg-background/95 backdrop-blur` + bottom border; left = Wulf mark + "Pulse" wordmark linked to `/mobile/dashboard`; no page title in header
- [ ] **SHELL-03**: Header right slot — `Bell` icon button (placeholder, no menu/badge, `aria-label="Notifications"`, empty `onClick`, keyboard-accessible)
- [ ] **SHELL-04**: Header right slot — compact user avatar (`h-7 w-7`); tapping opens the More drawer
- [x] **SHELL-05**: `<main>` content area is scrollable with bottom padding equal to bottom-nav height + safe-area inset
- [ ] **SHELL-06**: Fixed bottom nav: `border-t bg-background`, `max-w-lg mx-auto` wrapper, 5 cells (4 tabs + More)
- [ ] **PAX8-03**: Pulse syncs PAX8 companies into Postgres
- [ ] **PAX8-04**: Pulse syncs PAX8 subscriptions (product, seat count, billing term) into Postgres
- [ ] **PAX8-05**: Pulse syncs PAX8 product catalog (SKUs, categories) into Postgres, so subscriptions are human-readable
- [ ] **PAX8-06**: Pulse syncs PAX8 orders/invoices (historical line items) into Postgres, enabling cost reconciliation over time, not just current-state seats
- [ ] **PAX8-07**: Sync runs on a daily schedule via the existing `sync-scheduler.ts` cron pattern
- [ ] **PAX8-08**: All PAX8 sync operations are read-only — no writes back to the PAX8 API (no seat adjustments, no order placement)
- [ ] **PAX8-09**: PAX8 integration can be toggled on/off via `/admin/integrations`, consistent with other integrations (`integration_settings` table)
### NAV — Bottom tab bar (spec §3.1)
### PAX8 — Company Matching
- [ ] **NAV-01**: Four equal-width primary tabs: Dashboard (`LayoutDashboard`), Tickets (`Ticket`), Finance (`DollarSign`), Analyzer (`Sparkles`)
- [ ] **NAV-02**: Tabs route to `/mobile/dashboard`, `/mobile/tickets`, `/mobile/finance`, `/mobile/analyzer`
- [ ] **NAV-03**: Active state uses `text-primary`, inactive uses `text-muted-foreground`; active detection via `pathname.startsWith(href)`
- [ ] **PAX8-10**: PAX8 companies are automatically matched to existing Autotask companies by fuzzy name similarity at sync time
- [ ] **PAX8-11**: Unmatched or ambiguous company matches are flagged rather than silently guessed
- [ ] **PAX8-12**: An admin can view flagged/ambiguous company matches and manually resolve them to the correct Autotask company
### DRAWER — More Sheet drawer (spec §3.2)
### PAX8 — UI Surface
- [ ] **DRAWER-01**: Fifth bottom-bar control labeled "More" with `Menu` icon opens a shadcn `Sheet`
- [ ] **DRAWER-02**: Sheet uses a single consistent side (`right` or `bottom`) — pick one and stay consistent
- [ ] **DRAWER-03**: Drawer top section "Mobile sections" lists Engagement (`/mobile/engagement`)
- [ ] **DRAWER-04**: Drawer middle section "Full site" lists desktop-only pages (Quotes, Configuration Items, Backup Status, Ticket Digest, Admin / Sync) each with `ExternalLink` icon
- [ ] **DRAWER-05**: Drawer bottom section "Account" shows current user (avatar + email, read-only) and a Sign out action that calls `signOut()` then `router.push('/auth/sign-in')`
- [x] **DRAWER-06**: `app/mobile/nav/page.tsx` is deleted in the same change that ships the drawer
- [ ] **PAX8-13**: A new `/pax8` page lists PAX8 companies with their subscriptions and a cost breakdown
- [ ] **PAX8-14**: The `/pax8` page surfaces flagged/ambiguous company matches (PAX8-11) for manual resolution
### DASH — Mobile Dashboard (spec §6.1)
## Future Requirements
- [ ] **DASH-01**: 2×2 KPI grid with four primary metric cards drawn from desktop dashboard hero stats
- [ ] **DASH-02**: "Needs Attention" horizontal-scroll strip of compact cards (overdue tickets, failed backups, stalled workflows); tapping a card opens its detail view
- [ ] **DASH-03**: Compact backup/worker status row showing analyzer worker, RMM worker, and backup-success-rate; read-only; tap opens desktop admin page
- [ ] **DASH-04**: No charts/recharts on the mobile Dashboard
<!-- Deferred — not this milestone. -->
### TICK — Mobile Tickets list (spec §6.2)
- [ ] **TICK-01**: Collapsible filter strip at top (`Collapsible` from shadcn), default collapsed; expanded shows status, priority, queue, assigned-to-me toggle
- [ ] **TICK-02**: Filter state syncs to URL query string for deep-linking
- [ ] **TICK-03**: List rows have left-edge color stripe by priority (Critical/High/Medium/Low → red/orange/amber/slate); body shows ticket #, title, company, age, assignee
- [ ] **TICK-04**: Single-tap on a row opens detail page
- [ ] **TICK-05**: Cursor-based infinite scroll (~25 per page) replaces pagination; next page triggers when last row enters viewport via IntersectionObserver
- [ ] **TICK-06**: "Load more" fallback button present for accessibility
- [ ] **TICK-07**: Detail page (`/mobile/tickets/[id]`) header reskinned to match new shell (Wulf mark, breadcrumb back); body kept largely as-is
### FIN — Mobile Finance (spec §6.3)
- [x] **FIN-01**: Page restyled with new Card and typography scale; spacing fixed for small phones
- [x] **FIN-02**: Wide tables on mobile widths replaced with stacked lists; no new data, no new sections
### ANL — Mobile Analyzer feed (spec §6.4) — NEW PAGE
- [ ] **ANL-01**: `/mobile/analyzer` route exists (read-only feed, most-recent-first stream of AI ticket analyses)
- [ ] **ANL-02**: Each list row shows ticket #, title, analyzer one-line summary, confidence badge, stage indicator (Triage → Analyze → Deep Review)
- [ ] **ANL-03**: Tapping a row opens a mobile summary view rendering Summary, Next Step, Next Step Rationale (all already produced by the analyzer pipeline)
- [ ] **ANL-04**: Summary view includes "View full analysis" link out to the desktop analyzer page
- [ ] **ANL-05**: No editing, no re-run, no prompt tuning on mobile
- [ ] **ANL-06**: Source data via `/api/mobile/analyzer/feed` (or reuse an existing list endpoint if it returns the right shape) reading from `analyzer_analyses`
### ENG — Mobile Engagement (spec §6.5) — NEW PAGES
- [ ] **ENG-01**: `/mobile/engagement` overview page (real refactor, not a thin adaptation of the ~1300-line desktop page)
- [ ] **ENG-02**: Period selector chip row (today / 7d / 30d) sticky just below the page H1
- [ ] **ENG-03**: Summary cards stacked single-column (active users, total Graph hours, total Autotask hours, hours-per-active-user) — no 4-up grid on phone widths
- [ ] **ENG-04**: Per-employee list as stacked rows (avatar/initials, name, role, hours bar) with sort control above (sort by hours, name, utilization) and search input
- [ ] **ENG-05**: Top of list shows compact "hours trend" sparkline scoped to the selected period; no multi-series chart on mobile
- [ ] **ENG-06**: User profile is `/mobile/engagement/[userId]` (segment form preferred for shareable URLs); single-column layout: identity header → period selector → key metrics (compact) → activity breakdown list → recent items
- [ ] **ENG-07**: User profile is a real page, not a modal — replaces desktop user-detail modal pattern on mobile so back gesture works
- [ ] **ENG-08**: Profile reuses existing engagement profile data endpoints; no new data
- [ ] **ENG-09**: Engagement is reachable from the More drawer, NOT the bottom bar
### TZ — User Timezone (Phase 7.1, urgent insertion)
- [ ] **TZ-01**: Better Auth users table extended with a `timezone` field (IANA string). Default for users with no value = `process.env.DEFAULT_TIMEZONE || 'UTC'`. Existing rows backfill to the default; UTC remains the storage timezone for all date columns
- [ ] **TZ-02**: Date math that powers dashboards, ticket filters, finance views, and engagement period selectors computes day/week boundaries against the viewing user's `timezone` — NOT UTC, NOT the browser's local zone (browsers may differ from the user's chosen tz, e.g. travel) (Exception: `engagement_snapshots`-derived metrics — active users D7/D30/D90, MS Graph hours — bucket by UTC at sync time and remain UTC-bucketed in this phase; ≤24h drift is accepted because engagement is an admin-overview surface, not an operational date display. Per-user snapshot bucketing is deferred to a future phase if needed.)
- [ ] **TZ-03**: `GET /api/me/timezone` (auth required) returns `{ timezone, source: 'user' | 'default' }`. `PUT /api/me/timezone` accepts `{ timezone }`, validates against `Intl.supportedValuesOf('timeZone')`, persists to the user row, returns the new value
- [ ] **TZ-04**: Shared client hook `useUserTimezone()` reads tz from `useSession()` (Better Auth additionalField). All date-formatting and range-bucketing in mobile + desktop pages goes through this hook — no scattered `Intl.DateTimeFormat` instantiations with hardcoded zones
### PROF — Profile page surface (Phase 9)
- [ ] **PROF-01**: `/mobile/profile` exists as a real page (not a modal), gated by `requireAuth()`, accessible from the More drawer
- [ ] **PROF-02**: Page renders four sections in this order, each a shadcn `Card`: Timezone, Theme, Notifications, Channels
- [ ] **PROF-03**: Per-section save model — switch/select toggles save on change (debounced 400ms for the notifications matrix); text inputs save on blur or via an explicit Save button next to the input. Sonner toast confirms each save; errors render inline and do NOT optimistically update
- [ ] **PROF-04**: `MoreDrawer.tsx` Account section gains a "Profile & preferences" link (routes to `/mobile/profile`) above the Sign-out destructive action; the existing user-identity row becomes the tappable link
### TZ-CHOOSER — Timezone chooser UI (Phase 9)
- [ ] **TZ-CHOOSER-01**: Profile Timezone section uses a shadcn `Combobox` (or `Select` if list overflows) populated from `Intl.supportedValuesOf('timeZone')` plus the `EXTRA_ALLOWED_TIMEZONES` constants (`UTC`, `Etc/UTC`, `GMT`, `Etc/GMT`); writes via the existing `PUT /api/me/timezone` endpoint unchanged
- [ ] **TZ-CHOOSER-02**: Currently-rendering time displayed alongside as read-only (e.g., "Your current time: 2026-05-09 14:32 in America/Chicago"), formatted via `useUserTimezone()`
### THEME — Theme persistence (Phase 9)
- [ ] **THEME-01**: `theme TEXT NOT NULL DEFAULT 'system'` column added to `user` table (allowed values: `'light'` | `'dark'` | `'system'`); existing rows backfilled to `'system'`; exposed via Better Auth `additionalFields` so `session.user.theme` is available the same way `session.user.timezone` is today
- [ ] **THEME-02**: `GET /api/me/theme` (auth required) returns `{ theme, source: 'user' | 'default' }`. `PUT /api/me/theme` accepts `{ theme }`, validates against the three-string allowlist, writes session.user.id only, returns the new value
- [ ] **THEME-03**: On session load and after sign-in, a client-side effect compares `session.user.theme` against `useTheme()` and calls `setTheme(session.user.theme)` if different — server is canonical; brief flash on auth boundary is acceptable
- [ ] **THEME-04**: Existing desktop `ThemeToggle` (`components/theme-toggle.tsx`) becomes session-aware: the `setTheme()` callback also issues `PUT /api/me/theme` so the desktop affordance writes through to the server
- [ ] **THEME-05**: Default value for users who never visit the settings page is `'system'`; next-themes handles OS-preference detection at render time — zero behavior change for existing signed-in users
### CHAN — Personal notification channels (Phase 9)
- [ ] **CHAN-01**: `notification_channels.owner_user_id TEXT REFERENCES "user"(id) ON DELETE CASCADE` column added; `NULL` = global channel (existing rows), `NOT NULL` = personal channel
- [ ] **CHAN-02**: A user has at most one personal Teams channel and one personal ntfy channel at a time. The constraint is enforced in the API layer (UPSERT keyed by `(owner_user_id, channel_type)`) rather than via a partial-unique index
- [ ] **CHAN-03**: ntfy topic is **Pulse-minted** on first save — API generates a UUID-prefixed topic (e.g., `pulse-7f3a9c2b`) and returns it. UI surfaces the subscribe link (`https://ntfy.sh/<topic>`) and a QR code so the user can subscribe in their ntfy app. Power users can override via an "Edit advanced" disclosure with a custom topic string
- [ ] **CHAN-04**: Teams webhook URL is user-supplied free text, validated as an `https://` URL whose host matches `*.webhook.office.com` or `*.logic.azure.com`
- [ ] **CHAN-05**: On save (Teams URL or first ntfy mint) the API issues a single best-effort test send ("Pulse channel verified — you can ignore this message."). The save itself succeeds even if the test fails; the test result (success / HTTP status / error message) surfaces inline next to the input
- [ ] **CHAN-06**: Admins (role `admin` or `super-admin`) can read AND edit any user's personal channels via `/admin/workflow/channels` (extended with an Owner column + filter). Non-admin users only see/edit their own personal channels
- [ ] **CHAN-07**: `/api/me/channels` exposes the user's personal channels — `GET` returns the configured set, `PUT /api/me/channels/teams` and `PUT /api/me/channels/ntfy` upsert the row, `DELETE /api/me/channels/{type}` removes one, `POST /api/me/channels/{type}/test` issues a test send. All routes auth-gated to `session.user.id`
### SUB — Per-event notification subscriptions (Phase 9)
- [ ] **SUB-01**: New table `notify_event_keys` (`key TEXT PRIMARY KEY`, `display_label TEXT NOT NULL`, `description TEXT`, `sort_order INTEGER DEFAULT 0`, `is_active BOOLEAN DEFAULT true`, `created_at TIMESTAMP DEFAULT NOW()`); admins manage it at a new `/admin/workflow/event-keys` page (CRUD on label/description/sort/active). Keys not in the lookup are still routable — the lookup is a humanization layer, not a gate
- [ ] **SUB-02**: New table `user_event_subscriptions` (`user_id TEXT REFERENCES "user"(id) ON DELETE CASCADE`, `event_key TEXT NOT NULL`, `channel_type VARCHAR(20) NOT NULL`, `enabled BOOLEAN NOT NULL DEFAULT true`, `updated_at TIMESTAMP NOT NULL DEFAULT NOW()`, `PRIMARY KEY (user_id, event_key, channel_type)`). Row absence = default enabled (opt-out model)
- [ ] **SUB-03**: Profile Notifications section renders a matrix — rows = active `notify_event_keys`, columns = personal channel types the user has configured. When only one personal channel is configured, the matrix collapses to a single Channel column. New event keys go live for everyone immediately (default enabled)
- [ ] **SUB-04**: `/api/me/notification-subscriptions``GET` returns the full matrix for the calling user (joining active event keys with stored rows, defaulting missing rows to enabled), `PUT` writes a single row. Auth-gated to `session.user.id`
### ROUTE — notify.ts per-user routing (Phase 9)
- [ ] **ROUTE-01**: The `notify` step config gains an optional `route_to_user` block: `{ source, field, resolve, event_key, channel_type? }`. When absent, behavior is unchanged (backward compatible). When present, it is attempted before falling back to the step's existing `channel_id`
- [ ] **ROUTE-02**: Resolvers shipped in v1 — `autotask_resource_email` (joins `resources.email` from a resource ID), `direct_email` (the field IS already an email), `pulse_user_id` (the field IS already a Pulse `user.id`). Resolvers live in `lib/services/pipeline-steps/notify-resolvers.ts` and are registered in a `Map<string, Resolver>` so adding a resolver is a one-file change
- [ ] **ROUTE-03**: notify.ts user-route order when `route_to_user` is present: read `context[source][field]` → call resolver → look up Pulse user → check `user_event_subscriptions(event_key, channel_type)` → look up personal channel → send. Each branch can short-circuit per the muting / fallback rules below
- [ ] **ROUTE-04**: When the user route can't deliver (no personal channel of the requested type, or send returned non-2xx), notify.ts falls back to the step's `channel_id` (global) and records `output.user_route_fallback = { reason: 'no_channel' | 'send_failed' | 'user_not_found', user_id?, channel_type, error? }` on the execution-step row. Step still returns `success: true` (the fallback succeeded)
- [ ] **ROUTE-05**: When the user has the relevant `(event_key, channel_type)` toggle DISABLED, notify.ts records `output.skipped_reason = 'user_muted'` and does NOT fall back to global — muting must actually mute. Step returns `success: true` (intended skip)
- [ ] **ROUTE-06**: When `channel_type` is omitted in `route_to_user`, notify.ts attempts `ntfy` first, then `teams`, then global fallback — favoring push semantics for managers on mobile
- [ ] **ROUTE-07**: `/admin/workflow/executions` page gains a filter "Show executions that fell back to global" — a one-line UI addition that surfaces `user_route_fallback` events so admins can repair broken personal channels
## v2 Requirements
Acknowledged but deferred. Not in this milestone's roadmap.
### NOTIF — Notifications
- **NOTIF-01**: Real notification list behind the Bell icon (replaces SHELL-03 placeholder)
- **NOTIF-02**: Notification badge logic on the Bell icon
### TABLET — Tablet breakpoint
- **TABLET-01**: `md:max-w-2xl mx-auto` wrapper for tablet widths
### OFFLINE — Offline support
- **OFFLINE-01**: Service worker for offline cache
- **OFFLINE-02**: Push notifications (requires SW)
### EDIT — Mobile editing
- **EDIT-01**: Mobile editing on Engagement user detail
- **EDIT-02**: Mobile re-run / prompt edits on Analyzer
- Write access to PAX8 (seat adjustments, order placement) — no concrete need identified yet; revisit only if reconciliation surfaces one
- Domain-based or external-ID-based company matching, if research resolves [[RESEARCH-pax8-company-identifiers]] in `.planning/research/questions.md` and finds a more stable join key than name
- General Pulse data assistant (chatbot) consuming this data — separate future milestone, see `.planning/seeds/SEED-003-general-pulse-data-assistant.md`
## Out of Scope
Explicitly excluded for v1. Documented to prevent scope creep.
<!-- Explicit exclusions with reasoning. -->
| Feature | Reason |
|---------|--------|
| Service worker / offline cache / push notifications | No clear offline use-case yet — defer until one lands (spec §4, §7) |
| Tablet breakpoint (`md:max-w-2xl`) | Noted as follow-up; keep `max-w-lg` for v1 (spec §4, §7) |
| Real notification list behind the Bell | Placeholder only this iteration; future phase wires it (spec §5.1, §7) |
| Mobile editing on Engagement user detail | Read-only on mobile by design (spec §6.5, §7) |
| Mobile re-run / prompt tuning on Analyzer | Read-only on mobile by design (spec §6.4, §7) |
| Charts / recharts on mobile Dashboard | Not earning their weight on small widths (spec §6.1, §7) |
| Restyling/replacing desktop pages reachable from More drawer | Desktop pages stay as-is (spec §7) |
| Multi-series chart on mobile Engagement overview | Replaced by single sparkline (spec §6.5) |
| Modal-based user detail on mobile | Replaced by real page so back gesture works (spec §6.5) |
| `/mobile-v2` parallel directory | Rebuild `/mobile` in place — keep canonical URLs (spec §2) |
| Per-user-tz `engagement_snapshots` bucketing (TZ-02 carve-out) | Engagement metrics derived from `engagement_snapshots` (active users D7/D30/D90, MS Graph hours) are bucketed by UTC at sync time. Per-user re-bucketing would require either per-request re-bucket (expensive) or per-user snapshot rebuild (doubles storage). ≤24h drift accepted on this admin-overview surface. May revisit in a future phase. |
- Write-back to PAX8 — this integration is read-only in v2.0, matching the pattern of most existing Pulse integrations (RMM, IT Glue, Veeam)
- Chatbot/NL query layer — deliberately separated into SEED-003 so this milestone stays scoped to the data sync itself
## Traceability
Updated during roadmap creation.
| Requirement | Phase | Status |
|-------------|-------|--------|
| PWA-01 | Phase 1 | Pending |
| PWA-02 | Phase 1 | Pending |
| PWA-03 | Phase 1 | Pending |
| PWA-04 | Phase 1 | Pending |
| SHELL-01 | Phase 2 | Complete |
| SHELL-02 | Phase 2 | Pending |
| SHELL-03 | Phase 2 | Pending |
| SHELL-04 | Phase 2 | Pending |
| SHELL-05 | Phase 2 | Complete |
| SHELL-06 | Phase 2 | Pending |
| NAV-01 | Phase 2 | Pending |
| NAV-02 | Phase 2 | Pending |
| NAV-03 | Phase 2 | Pending |
| DRAWER-01 | Phase 2 | Pending |
| DRAWER-02 | Phase 2 | Pending |
| DRAWER-03 | Phase 2 | Pending |
| DRAWER-04 | Phase 2 | Pending |
| DRAWER-05 | Phase 2 | Pending |
| DRAWER-06 | Phase 2 | Complete |
| DASH-01 | Phase 3 | Pending |
| DASH-02 | Phase 3 | Pending |
| DASH-03 | Phase 3 | Pending |
| DASH-04 | Phase 3 | Pending |
| TICK-01 | Phase 4 | Pending |
| TICK-02 | Phase 4 | Pending |
| TICK-03 | Phase 4 | Pending |
| TICK-04 | Phase 4 | Pending |
| TICK-05 | Phase 4 | Pending |
| TICK-06 | Phase 4 | Pending |
| TICK-07 | Phase 4 | Pending |
| FIN-01 | Phase 5 | Complete |
| FIN-02 | Phase 5 | Complete |
| ANL-01 | Phase 6 | Pending |
| ANL-02 | Phase 6 | Pending |
| ANL-03 | Phase 6 | Pending |
| ANL-04 | Phase 6 | Pending |
| ANL-05 | Phase 6 | Pending |
| ANL-06 | Phase 6 | Pending |
| ENG-01 | Phase 7 | Pending |
| ENG-02 | Phase 7 | Pending |
| ENG-03 | Phase 7 | Pending |
| ENG-04 | Phase 7 | Pending |
| ENG-05 | Phase 7 | Pending |
| ENG-09 | Phase 7 | Pending |
| ENG-06 | Phase 8 | Pending |
| ENG-07 | Phase 8 | Pending |
| ENG-08 | Phase 8 | Pending |
**Coverage:**
- v1 requirements: 47 total
- Mapped to phases: 47
- Unmapped: 0 ✓
**Per-phase counts:**
- Phase 1 (PWA Scaffolding): 4 requirements
- Phase 2 (Mobile Shell + More Drawer): 15 requirements
- Phase 3 (Dashboard Restyle): 4 requirements
- Phase 4 (Tickets Restyle): 7 requirements
- Phase 5 (Finance Restyle): 2 requirements
- Phase 6 (Analyzer Feed): 6 requirements
- Phase 7 (Engagement Overview): 6 requirements
- Phase 8 (Engagement User Profile): 3 requirements
- Phase 9 (User Profile & Preferences): 31 requirements (PROF-01..04, TZ-CHOOSER-01..02, THEME-01..05, CHAN-01..07, SUB-01..04, ROUTE-01..07)
<!-- Filled in by the roadmapper when the roadmap is created. -->
---
*Requirements defined: 2026-05-03*
*Last updated: 2026-05-09 — Phase 9 requirements added (PROF, TZ-CHOOSER, THEME, CHAN, SUB, ROUTE) after `/gsd-discuss-phase 9`*
</content>
</invoke>
*Requirements defined: 2026-07-10*