wulf-pulse/.planning/REQUIREMENTS.md

21 KiB
Raw Blame History

Requirements: Pulse Mobile Shell Redesign

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

v1 Requirements

Requirements for this milestone. Each maps to a spec section and a roadmap phase.

PWA — Progressive Web App scaffolding (spec §4)

  • 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)

Shell — /mobile layout (spec §5)

  • 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
  • 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)

NAV — Bottom tab bar (spec §3.1)

  • 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)

DRAWER — More Sheet drawer (spec §3.2)

  • 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')
  • DRAWER-06: app/mobile/nav/page.tsx is deleted in the same change that ships the drawer

DASH — Mobile Dashboard (spec §6.1)

  • 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

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)

  • FIN-01: Page restyled with new Card and typography scale; spacing fixed for small phones
  • 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-subscriptionsGET 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

Out of Scope

Explicitly excluded for v1. Documented to prevent scope creep.

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.

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)

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