# Roadmap: Pulse

## Milestones

- ✅ **v1.0 Mobile Shell Redesign** — Phases 1-9.1 (shipped 2026-07-10)
- 📋 **v2.0 PAX8 Integration** — Phases 10-14 (planned)

## Phases

**Phase Numbering:**
- Integer phases (1, 2, 3): Planned milestone work
- Decimal phases (2.1, 2.2): Urgent insertions (marked with INSERTED)

Decimal phases appear between their surrounding integers in numeric order.

<details>
<summary>✅ v1.0 Mobile Shell Redesign (Phases 1-9.1) - SHIPPED 2026-07-10</summary>

Eight phases mirror the deliberate build order in the source spec
(`docs/superpowers/specs/2026-05-03-mobile-shell-design.md` §8). Each phase
shipped independently to `master` — no big-bang merge. Phase 1 laid PWA
metadata and safe-area utilities. Phase 2 rebuilt `app/mobile/layout.tsx`
with the new header, 5-cell bottom nav, and More drawer (deleting
`/mobile/nav` in the same change). Once the shell landed, Phases 3–7 were
independent restyles/new pages; Phase 8 followed Phase 7 because the user
profile is reached from the Engagement overview. All work happened in place
under `/mobile/*` — no `/mobile-v2`, no parallel routes.

- [x] **Phase 1: PWA Scaffolding** — Manifest, viewport meta, and safe-area utilities so the shell installs and paints under the home indicator
- [x] **Phase 2: Mobile Shell + More Drawer** — New `app/mobile/layout.tsx` (header + 5-cell bottom nav) and Sheet drawer that replaces `/mobile/nav`
- [x] **Phase 3: Dashboard Restyle** — 2×2 KPI grid, Needs Attention strip, worker/backup status row (no charts)
- [x] **Phase 4: Tickets Restyle** — Collapsible URL-synced filters, priority-bar rows, cursor-based infinite scroll, detail header reskin
- [x] **Phase 5: Finance Restyle** — Adopt new Card + typography scale, swap wide tables for stacked lists (completed 2026-05-03)
- [x] **Phase 6: Analyzer Feed (NEW)** — `/mobile/analyzer` read-only stream + `/api/mobile/analyzer/feed`
- [x] **Phase 7: Engagement Overview (NEW)** — `/mobile/engagement` phone-first overview reachable from the More drawer
- [x] **Phase 7.1: User Timezone Fix (INSERTED — urgent)** — Per-user IANA timezone column + viewer-tz date math so dashboards and filters render the right "today"
- [x] **Phase 8: Engagement User Profile (NEW)** — `/mobile/engagement/[userId]` real-page profile that replaces the desktop modal pattern
- [x] **Phase 9: User Profile & Preferences (NEW)** — `/mobile/profile` settings page (timezone chooser, theme, mobile push, Teams + ntfy channels)
- [x] **Phase 9.1: ntfy Backend Fix (INSERTED — urgent)** — Personal ntfy channels target the company ntfy server with bearer auth + `pulse-me-` prefix (UAT gap closure)

### Phase 1: PWA Scaffolding
**Goal**: A manager who taps "Add to Home Screen" gets a standalone Pulse icon that opens to the mobile shell with content respecting the device safe areas.
**Depends on**: Nothing (first phase)
**Requirements**: PWA-01, PWA-02, PWA-03, PWA-04
**Success Criteria** (what must be TRUE):
  1. Visiting `/manifest.json` returns valid JSON with `name: "Pulse"`, `display: "standalone"`, `start_url: "/mobile"`, and theme/background colors matching the app shells
  2. The root `app/layout.tsx` references the manifest via `<link rel="manifest">` and the viewport meta includes `viewport-fit=cover`
  3. A safe-area utility (Tailwind arbitrary values or shared class) is available so any sticky top/bottom bar can opt into `env(safe-area-inset-top)` / `env(safe-area-inset-bottom)` padding
  4. Installing Pulse to a phone home screen launches a chromeless app pointed at `/mobile` (no service worker, no offline)
**Plans**: 2 plans
- [x] 01-01-PLAN.md — Web App Manifest + viewport-fit=cover (PWA-01, PWA-02, PWA-03)
- [x] 01-02-PLAN.md — Safe-area `pt-safe` / `pb-safe` @utility blocks in brand.css (PWA-04, gap closure)
**UI hint**: no

### Phase 2: Mobile Shell + More Drawer
**Goal**: Every `/mobile/*` page renders inside a new layout — sticky header (Wulf mark + Bell placeholder + avatar), scrollable content, and a 5-cell bottom nav whose fifth control opens a Sheet drawer that fully replaces `/mobile/nav`.
**Depends on**: Phase 1
**Requirements**: SHELL-01, SHELL-02, SHELL-03, SHELL-04, SHELL-05, SHELL-06, NAV-01, NAV-02, NAV-03, DRAWER-01, DRAWER-02, DRAWER-03, DRAWER-04, DRAWER-05, DRAWER-06
**Success Criteria** (what must be TRUE):
  1. On any `/mobile/*` route the user sees a sticky header with the Wulf wordmark linking to `/mobile/dashboard`, a Bell icon button (keyboard-focusable, no menu), and a compact avatar — no page title in the header
  2. A fixed bottom bar exposes four primary tabs (Dashboard, Tickets, Finance, Analyzer) plus a More cell; tapping a tab routes to its page and the active tab uses `text-primary` based on `pathname.startsWith(href)`
  3. Tapping More (or the header avatar) opens a single Sheet drawer with three sections — Mobile sections (Engagement), Full site (Quotes, Configuration Items, Backup Status, Ticket Digest, Admin/Sync — each with an `ExternalLink` hint), and Account (current user read-only + Sign out)
  4. Tapping Sign out in the drawer signs the user out and lands them on `/auth/sign-in`
  5. `app/mobile/nav/page.tsx` no longer exists; visiting `/mobile/nav` does not render the old standalone nav page
  6. Page content scrolls under the sticky header and is not hidden behind the bottom nav (bottom padding accounts for nav height + safe-area inset)
**Plans**: 2 plans
- [x] 02-01-PLAN.md — Build mobile shell components (HeaderBar, BottomNav, MoreDrawer) + analyzer placeholder (SHELL-02..04, SHELL-06, NAV-01..03, DRAWER-01..05)
- [x] 02-02-PLAN.md — Wire new components into app/mobile/layout.tsx, delete app/mobile/nav/page.tsx (SHELL-01, SHELL-05, DRAWER-06)
**UI hint**: yes

### Phase 3: Dashboard Restyle
**Goal**: A manager opening `/mobile/dashboard` sees the state of the business at a glance — four KPIs, items needing attention, and a worker/backup status row — with no charts.
**Depends on**: Phase 2
**Requirements**: DASH-01, DASH-02, DASH-03, DASH-04
**Success Criteria** (what must be TRUE):
  1. Dashboard renders a 2×2 grid of four primary KPI cards drawn from desktop hero stats (no 1×4 row, no charts)
  2. Below the grid, a "Needs Attention" horizontally-scrollable strip surfaces overdue tickets, failed backups, and stalled workflows; tapping a card opens its detail view
  3. A compact status row shows analyzer worker, RMM worker, and backup-success-rate; tapping any element opens the corresponding desktop admin page
  4. The page contains no recharts/chart components on phone widths
**Plans**: 2 plans
- [x] 03-01-PLAN.md — /api/mobile/dashboard reshape + KpiCardMobile/NeedsAttentionStrip/WorkerStatusRow components (DASH-01, DASH-02, DASH-03)
- [x] 03-02-PLAN.md — Replace /mobile/dashboard page body with 3-section layout, no charts (DASH-01, DASH-02, DASH-03, DASH-04)
**UI hint**: yes

### Phase 4: Tickets Restyle
**Goal**: A manager triages tickets on a phone with a collapsible filter bar that deep-links via URL, priority-coloured rows, and infinite scroll — and the detail page header matches the new shell.
**Depends on**: Phase 2
**Requirements**: TICK-01, TICK-02, TICK-03, TICK-04, TICK-05, TICK-06, TICK-07
**Success Criteria** (what must be TRUE):
  1. The Tickets page opens with the filter strip collapsed; expanding it reveals status, priority, queue, and an assigned-to-me toggle, and changing any filter updates the URL query string (deep link works on reload)
  2. Each list row has a left-edge stripe matching priority (Critical/High/Medium/Low → red/orange/amber/slate) and shows ticket #, title, company, age, and assignee
  3. Single-tapping a row navigates to `/mobile/tickets/[id]`
  4. Scrolling to the bottom of the list automatically loads the next ~25 rows (no Next button); a "Load more" fallback button is also visible/focusable for accessibility
  5. The detail page header uses the new shell styling (Wulf mark, breadcrumb back) while the body remains largely unchanged
**Plans**: 3 plans
- [x] 04-01-PLAN.md — /api/mobile/tickets cursor rewrite + TicketFilterStrip + TicketRowSkeleton components (TICK-01, TICK-02, TICK-05)
- [x] 04-02-PLAN.md — Replace app/mobile/tickets/page.tsx with URL-synced filters, priority-stripe rows, IntersectionObserver infinite scroll (TICK-01..TICK-06)
- [x] 04-03-PLAN.md — Reskin in-page header of app/mobile/tickets/[id]/page.tsx (back chevron + breadcrumb + ExternalLink) (TICK-07)
**UI hint**: yes

### Phase 5: Finance Restyle
**Goal**: A manager reading AR / invoice / payment status on a phone sees properly spaced cards and stacked lists instead of squished wide tables — same data, new shell.
**Depends on**: Phase 2
**Requirements**: FIN-01, FIN-02
**Success Criteria** (what must be TRUE):
  1. `/mobile/finance` adopts the new Card and typography scale — no horizontal overflow, spacing legible on small phones
  2. Sections that previously rendered wide tables on phone widths now render as stacked lists (no new sections, no new data sources)
**Plans**: 2 plans
- [x] 05-01-PLAN.md — FinanceRow + FinanceSkeleton helper components (FIN-01, FIN-02)
- [x] 05-02-PLAN.md — Rewrite app/mobile/finance/page.tsx to KPI grid + stacked lists + shadcn Collapsibles (FIN-01, FIN-02)
**UI hint**: yes

### Phase 6: Analyzer Feed (NEW)
**Goal**: A manager taps the Analyzer tab and skims a most-recent-first stream of AI ticket analyses, opening any one to a phone-friendly summary view that links out to desktop for full details.
**Depends on**: Phase 2
**Requirements**: ANL-01, ANL-02, ANL-03, ANL-04, ANL-05, ANL-06
**Success Criteria** (what must be TRUE):
  1. Tapping the Analyzer tab in the bottom nav lands on `/mobile/analyzer` and shows a most-recent-first list of AI ticket analyses
  2. Each row shows ticket #, title, the analyzer's one-line summary, a confidence badge, and a stage indicator (Triage → Analyze → Deep Review)
  3. Tapping a row opens a mobile summary view rendering Summary, Next Step, and Next Step Rationale, with a "View full analysis" link out to the desktop analyzer page
  4. The mobile feed never exposes editing, re-run, or prompt-tuning controls (read-only by design)
  5. The list reads from `analyzer_analyses` via `/api/mobile/analyzer/feed` (or a reused list endpoint that already returns the right shape)
**Plans**: 3 plans
- [x] 06-01-PLAN.md — /api/mobile/analyzer/feed endpoint with cursor pagination + kiosk_settings scoping (ANL-01, ANL-02, ANL-06)
- [x] 06-02-PLAN.md — AnalyzerFeedRow/StagePips/ConfidenceBadge/RowSkeleton components + replace /mobile/analyzer placeholder with feed list page (ANL-01, ANL-02, ANL-05, ANL-06)
- [x] 06-03-PLAN.md — /mobile/analyzer/[id] detail page reading existing /api/analyzer/analyses/[id] (ANL-03, ANL-04, ANL-05)
**UI hint**: yes

### Phase 7: Engagement Overview (NEW)
**Goal**: A manager reaches Engagement from the More drawer and sees a phone-first overview — period chips, stacked summary cards, a sortable per-employee list, and one compact sparkline.
**Depends on**: Phase 2
**Requirements**: ENG-01, ENG-02, ENG-03, ENG-04, ENG-05, ENG-09
**Success Criteria** (what must be TRUE):
  1. The Mobile sections row in the More drawer links to `/mobile/engagement`; the Analyzer is on the bottom bar but Engagement is not
  2. The overview page shows a period selector (today / 7d / 30d) sticky just below the H1, with active period clearly indicated
  3. Summary cards (active users, total Graph hours, total Autotask hours, hours-per-active-user) render single-column stacked — no 4-up grid on phone widths
  4. The per-employee list renders as stacked rows (avatar/initials, name, role, hours bar) with a search input and a sort control above (sort by hours, name, utilization)
  5. A single compact "hours trend" sparkline renders at the top of the list, scoped to the selected period — no multi-series chart
**Plans**: 3 plans
- [x] 07-01-PLAN.md — /api/mobile/engagement/summary + /api/mobile/engagement/trend endpoints with period whitelist + requireAuth (ENG-03, ENG-05)
- [x] 07-02-PLAN.md — Engagement* mobile components (PeriodChips, SummaryCard, HoursSparkline, SortChips, SearchInput, UserRow, UserRowSkeleton + getInitials utility) (ENG-02, ENG-03, ENG-04, ENG-05)
- [x] 07-03-PLAN.md — app/mobile/engagement/page.tsx orchestration (period/sort state, IntersectionObserver, empty/error/not-configured states) (ENG-01, ENG-02, ENG-03, ENG-04, ENG-05, ENG-09)
**UI hint**: yes

### Phase 7.1: User Timezone Fix (INSERTED — urgent)
**Goal**: A user opening Pulse sees dashboards, filters, and "today/this week" date math computed in their own IANA timezone — not server UTC — so reports stop showing yesterday's data as today (and vice versa). Persistence layer remains UTC; only the read/display path changes.
**Depends on**: Nothing structural (Better Auth users table extension + read-path changes)
**Requirements**: TZ-01, TZ-02, TZ-03, TZ-04
**Success Criteria** (what must be TRUE):
  1. Each user has an IANA timezone (e.g. `America/New_York`) persisted server-side; default = `process.env.DEFAULT_TIMEZONE || 'UTC'` for users with no value yet
  2. Mobile and desktop dashboards, ticket filters, finance views, and engagement period selectors compute day/week boundaries against the viewer's timezone — not UTC and not the browser's local zone (browser zone may differ from the user's chosen zone, e.g. travel)
  3. Authenticated `GET /api/me/timezone` returns the user's tz; `PUT /api/me/timezone` accepts an IANA string and rejects anything not in `Intl.supportedValuesOf('timeZone')`
  4. A shared client hook (`useUserTimezone()`) reads the value from `useSession()` so all components use a single source of truth — no per-page `Intl` calls scattered around
  5. Existing UTC-stored data stays untouched (no destructive migration); only formatting and range-bucketing change
**Plans**: 6 plans
- [x] 07.1-01-PLAN.md — Add timezone column to user table + Better Auth additionalField (TZ-01)
- [x] 07.1-02-PLAN.md — /api/me/timezone GET + PUT with IANA validation (TZ-03)
- [x] 07.1-03-PLAN.md — Server-side read paths use user.timezone for day/week/month boundaries; auth-gates /api/mobile/finance; migrates /api/dashboard/trends (TZ-02)
- [x] 07.1-04-PLAN.md — useUserTimezone() client hook + reported-bug-surface mobile page migration + codebase-wide audit (TZ-04, TZ-02 client portion)
- [x] 07.1-05-PLAN.md — Codebase-wide useUserTimezone() adoption per the Plan 04 audit (TZ-04 SC#4 single-source-of-truth at codebase scale)
**UI hint**: no (this is a data/plumbing phase; the picker UI is part of Phase 9)

### Phase 8: Engagement User Profile (NEW)
**Goal**: From the Engagement overview, a manager taps an employee row and arrives at a real, shareable profile page — single-column phone-first — and the device back gesture returns them to the overview.
**Depends on**: Phase 7
**Requirements**: ENG-06, ENG-07, ENG-08
**Success Criteria** (what must be TRUE):
  1. Tapping a row in the per-employee list navigates to `/mobile/engagement/[userId]` (segment form, shareable URL)
  2. The profile is a real page (not a modal) — the device/browser back gesture returns to the overview at the same scroll position
  3. The profile renders single-column: identity header → period selector → key metrics (compact) → activity breakdown list → recent items, sourced from the existing engagement profile data endpoints (no new data)
**Plans**: 2 plans
- [x] 08-01-PLAN.md — MS Graph user-photo proxy at /api/mobile/engagement/user/[userId]/photo (ENG-06; D-25, D-26)
- [x] 08-02-PLAN.md — Mobile profile page at /mobile/engagement/[userId] + 6 EngagementProfile* components (ENG-06, ENG-07, ENG-08)
**UI hint**: yes

### Phase 9: User Profile & Preferences (NEW)
**Goal**: A logged-in user reaches a profile/settings page from the More drawer and can configure timezone (chooser UI for TZ-01), theme (light/dark/system, persisted server-side for cross-device consistency), mobile push notifications (per-event toggles, delivered via the ntfy phone app per the no-SW constraint), and personal notification channels (Teams webhook URL, Pulse-minted ntfy topic). Changes persist per-user and the existing notify pipeline routes through these per-user channels for events the user is subscribed to.
**Depends on**: Phase 7.1 (timezone schema), Phase 2 (More drawer)
**Requirements**: PROF-01, PROF-02, PROF-03, PROF-04, TZ-CHOOSER-01, TZ-CHOOSER-02, THEME-01, THEME-02, THEME-03, THEME-04, THEME-05, CHAN-01, CHAN-02, CHAN-03, CHAN-04, CHAN-05, CHAN-06, CHAN-07, SUB-01, SUB-02, SUB-03, SUB-04, ROUTE-01, ROUTE-02, ROUTE-03, ROUTE-04, ROUTE-05, ROUTE-06, ROUTE-07
**Canonical refs:**
- `docs/superpowers/specs/2026-05-03-mobile-shell-design.md` §4, §5.1, §7 (no-SW constraint, drawer Account section, deferred items)
- `lib/services/pipeline-steps/notify.ts` (existing channel-only notify; Phase 9 adds `route_to_user`)
- `lib/auth.ts` `additionalFields` (Phase 7.1 precedent for `theme` column exposure)
- `migrations/033_create_pipeline_engine_tables.sql` (`notification_channels`, `pipeline_steps` shapes)
- `migrations/083_add_user_timezone.sql` (column-on-user precedent from Phase 7.1)
- `app/api/me/timezone/route.ts` (per-user API conventions to mirror for `/api/me/theme`, `/api/me/channels`, `/api/me/notification-subscriptions`)
- `components/mobile/MoreDrawer.tsx` (Account section gains "Profile & preferences" link)
- `components/theme-toggle.tsx` + `components/theme-provider.tsx` (next-themes write-through path)
**Success Criteria** (what must be TRUE):
  1. Tapping Profile/Account in the More drawer routes to `/mobile/profile` (real page, not modal); the page renders four sections in order — Timezone, Theme, Notifications, Channels — each gated by `requireAuth()` and saving per-user (PROF-01..04)
  2. Theme persists server-side and applies on sign-in across devices via the `theme` column on `user` (Better Auth additionalFields), with next-themes still handling FOUC and the desktop `ThemeToggle` writing through to the server (THEME-01..05)
  3. Each user can configure one Teams webhook URL and one Pulse-minted ntfy topic; both are test-sent on save and admins have full read+edit access via `/admin/workflow/channels` (CHAN-01..07)
  4. The Notifications section renders a per-event × per-channel matrix sourced from `notify_event_keys`; defaults to enabled (opt-out model); writes via `/api/me/notification-subscriptions` (SUB-01..04)
  5. `lib/services/pipeline-steps/notify.ts` honors an optional `route_to_user` block on each notify step — resolving the user via a registered resolver, checking the subscription matrix, sending via the personal channel, and falling back to the step's `channel_id` on no-channel/send-failure (recorded as `user_route_fallback`) but skipping silently when the user has the toggle muted (ROUTE-01..07)
**Plans**: 6 plans
- [x] 09-01-PLAN.md — Schema foundation: theme column, owner_user_id, notify_event_keys, user_event_subscriptions (THEME-01, THEME-05, CHAN-01, SUB-01, SUB-02)
- [x] 09-02-PLAN.md — /api/me/* endpoints: theme, channels (Teams + ntfy), notification-subscriptions matrix (THEME-02, CHAN-02..05, CHAN-07, SUB-04)
- [x] 09-03-PLAN.md — notify.ts route_to_user branch + resolver registry + fallback semantics (ROUTE-01..06)
- [x] 09-04-PLAN.md — /mobile/profile UI part 1: page shell + drawer link + Timezone/Theme/Notifications Cards + Channels placeholder (PROF-01..04, TZ-CHOOSER-01..02, THEME-03, SUB-03)
- [x] 09-05-PLAN.md — /mobile/profile UI part 2: real Channels Card (Teams + ntfy + QR) + ThemeSessionBridge + ThemeToggle write-through (CHAN-02..05, CHAN-07, THEME-04)
- [x] 09-06-PLAN.md — Admin surfaces: channels Owner column + filter, event-keys CRUD page, NEW /admin/workflow/executions with fallback filter (CHAN-06, SUB-01, ROUTE-07)
**UI hint**: yes

### Phase 9.1: ntfy Backend Fix (INSERTED — urgent)
**Goal**: A logged-in user enabling mobile push from `/mobile/profile` gets a topic published to `https://ntfy.wulfconsulting.cloud` (not the public `ntfy.sh`) with bearer auth via `NTFY_PULSE_TOKEN`, using the `pulse-me-` reserved prefix so personal channels never collide with the `noc-*` / `soc-*` namespaces reserved for NOC/SOC operations.
**Depends on**: Phase 9 (personal channels feature must exist)
**Requirements**: CHAN-03, CHAN-05, CHAN-07, ROUTE-04 (gap closure — re-targeting the existing implementation)
**Source**: `.planning/phases/09-user-profile-preferences-new/09-HUMAN-UAT.md` Test 1 — diagnosed gap
**Success Criteria** (what must be TRUE):
  1. `mintNtfyTopic()` returns `pulse-me-XXXXXXXX`; `NTFY_TOPIC_RE` enforces `^pulse-me-[A-Za-z0-9-]{6,64}$`; custom topics matching `pulse-`, `noc-`, `soc-`, or arbitrary names are rejected
  2. All four ntfy publish paths used for personal channels (`sendChannelTest`, `pipeline-steps/notify.ts sendNtfy`, `pipeline-steps/approval.ts` ntfy branch, `ticket-digest-service.ts deliver()` ntfy branch) target `${NTFY_BASE_URL || 'https://ntfy.wulfconsulting.cloud'}` and send `Authorization: Bearer ${NTFY_PULSE_TOKEN}` when `channel.owner_user_id` is set
  3. Global / admin ntfy channels (`owner_user_id IS NULL`) preserve their existing `channel.config.server_url` / `channel.config.auth_token` behavior — out-of-scope per gap diagnosis
  4. `/mobile/profile` QR code and subscribe link target `process.env.NEXT_PUBLIC_NTFY_BASE_URL || 'https://ntfy.wulfconsulting.cloud'`; help line under custom-topic Input reads "Topic must start with `pulse-me-`"
  5. `npx tsc --noEmit --pretty` and `npx vitest run lib/services/pipeline-steps/notify.test.ts` both pass (mute semantics intact)
**Plans**: 1 plan
- [x] 09.1-01-PLAN.md — Personal-channels regex/prefix/bearer + propagate to notify/approval/digest send paths + ProfileChannelsSection QR & copy
**UI hint**: no (backend-heavy; one component edit for QR/link target)

</details>

### 📋 v2.0 PAX8 Integration (Planned)

**Milestone Goal:** Sync PAX8 licensing/subscription data into Pulse, read-only,
mapped to Autotask companies, so managers can see subscription costs and seat
counts alongside existing company data.

This milestone follows the codebase's existing external-integration pattern
(`<name>-client.ts` + `<name>-factory.ts` + numbered migration + sync service +
scheduler entry + admin toggle). Phase 10 stands up auth + schema in isolation
so the OAuth2 client-credentials flow is proven before anything is built on
top of it. Phase 11 syncs the "current state" entities (companies, catalog,
subscriptions). Phase 12 adds historical cost data (orders/invoices) and the
fuzzy-name company-matching pass, since matching needs companies to already
exist. Phase 13 wires the combined sync into the daily scheduler and the
`/admin/integrations` toggle — deliberately last among the backend phases so
it schedules the *complete* sync, not a partial one. Phase 14 ships the
`/pax8` page, which needs Phase 12's data and match state to have something to
render, including the manual-resolution workflow for flagged companies.

- [x] **Phase 10: PAX8 Client & Auth Foundation** — OAuth2 client-credentials auth, `isPax8Configured()`, and the PAX8 schema migration (completed 2026-07-10)
- [x] **Phase 11: Company, Catalog & Subscription Sync** — Read-only sync of current-state companies, product catalog, and subscriptions into Postgres (completed 2026-07-11)
- [x] **Phase 12: Orders/Invoices & Company Matching** — Historical cost sync plus fuzzy-name auto-matching (with flagging) of PAX8 companies to Autotask companies (completed 2026-07-11)
- [x] **Phase 13: Scheduler & Admin Toggle** — Daily `pax8-daily` cron entry and an on/off switch in `/admin/integrations` (completed 2026-07-11)
- [ ] **Phase 14: /pax8 UI Surface** — New page listing companies/subscriptions/cost breakdown, plus manual resolution of flagged company matches

## Phase Details

### Phase 10: PAX8 Client & Auth Foundation
**Goal**: Pulse can authenticate to the PAX8 API via OAuth2 client-credentials, and the Postgres schema for all four PAX8 entities exists — proving the integration pattern before any sync logic is built on top of it.
**Depends on**: Nothing (first phase of v2.0)
**Requirements**: PAX8-01, PAX8-02
**Success Criteria** (what must be TRUE):
  1. `lib/services/pax8-factory.ts` exports `isPax8Configured()`, returning `true` only when the PAX8 client ID and secret env vars are both set, `false` otherwise
  2. `getPax8Client()` performs an OAuth2 client-credentials token exchange against `api.pax8.com/v1` and successfully calls a read-only endpoint (e.g., list companies) using the resulting bearer token
  3. Calling the client with missing/invalid credentials throws a clear, typed error rather than failing silently or crashing the process — matching the existing `is<Name>Configured()` + throw-if-missing pattern used by other integrations
  4. A new numbered migration creates the PAX8 tables (companies, subscriptions, products/catalog, orders, and a company-match/review table) using `IF NOT EXISTS`, ready for Phase 11+ to populate
**Plans**: 3 plans
- [x] 10-01-PLAN.md — PAX8 types + OAuth2 client (token exchange, audience, cache) + factory (isPax8Configured/getPax8Client) + mocked tests (PAX8-01, PAX8-02)
- [x] 10-02-PLAN.md — migrations/091_pax8_tables.sql (6 PAX8 tables, IF NOT EXISTS) + apply to dev DB (PAX8-01, PAX8-02)
- [x] 10-03-PLAN.md — verify-pax8-auth.ts live auth-proof (SC#2) + CLAUDE.md/INTEGRATIONS.md docs (PAX8-01, PAX8-02)
**UI hint**: no

### Phase 11: Company, Catalog & Subscription Sync
**Goal**: PAX8 companies, the product catalog, and current subscriptions are synced into Postgres and are human-readable (not raw SKU IDs) — the "current state" half of the integration.
**Depends on**: Phase 10
**Requirements**: PAX8-03, PAX8-04, PAX8-05, PAX8-08
**Success Criteria** (what must be TRUE):
  1. Running the sync populates a companies table with every PAX8 company (PAX8 ID, name, and other identifying fields)
  2. Running the sync populates a product/catalog table (SKUs, categories) and a subscriptions table (product, seat count, billing term) per company
  3. A synced subscription row displays a readable product name and category by joining to the catalog table — not a bare SKU/product ID
  4. No code path in the PAX8 client or this sync service issues a write (POST/PUT/PATCH/DELETE) to the PAX8 API — every call is a read, verified by inspection of the client's exposed methods
**Plans**: 3 plans
- [x] 11-01-PLAN.md — Migration 092 subscription cost columns + extend pax8 types + read-only client pagination helpers (PAX8-04, PAX8-05, PAX8-08)
- [x] 11-02-PLAN.md — pax8-sync-service.ts (companies + subscriptions + referenced-only catalog + soft-delete reconciliation) + /api/pax8/sync fire-and-forget route (PAX8-03, PAX8-04, PAX8-05, PAX8-08)
- [x] 11-03-PLAN.md — Read-only invariant proof + live sync run DB verification checkpoint (PAX8-03, PAX8-04, PAX8-05, PAX8-08)
**UI hint**: no

### Phase 12: Orders/Invoices & Company Matching
**Goal**: Pulse has historical PAX8 cost data for reconciliation over time, and every PAX8 company is automatically linked to its Autotask counterpart or explicitly flagged for review — never silently guessed.
**Depends on**: Phase 11
**Requirements**: PAX8-06, PAX8-10, PAX8-11
**Success Criteria** (what must be TRUE):
  1. Running the sync populates an orders/invoices table with historical line items (not just current-state seat counts), enabling cost-over-time comparisons
  2. At sync time, each PAX8 company is automatically matched to an Autotask company by fuzzy name similarity when a sufficiently confident match exists, and the match is persisted
  3. A PAX8 company with no match, or with multiple similarly-scored Autotask candidates, is persisted with a flagged/needs-review status instead of being auto-assigned
  4. Re-running the sync does not overwrite a match that has already been manually confirmed/resolved (idempotent with respect to human decisions)
**Plans**: 5 plans
- [x] 12-01-PLAN.md — Migration 093 (pg_trgm + pax8_order_items/pax8_companies columns) + Pax8Invoice/Pax8InvoiceItem types (PAX8-06, PAX8-10, PAX8-11)
- [x] 12-02-PLAN.md — pax8-client listAllInvoices/listAllInvoiceItems + tests + live field-mapping spot-check (PAX8-06)
- [x] 12-03-PLAN.md — pax8-company-matcher.ts (pg_trgm similarity, 0.90 threshold, tie/empty/idempotency policy) + tests (PAX8-10, PAX8-11)
- [x] 12-04-PLAN.md — syncOrders + syncCompanyMatches wired into Pax8SyncService.fullSync + sync-service tests (PAX8-06, PAX8-10, PAX8-11)
- [x] 12-05-PLAN.md — Live full-sync verification of all 4 success criteria + human-verify checkpoint (PAX8-06, PAX8-10, PAX8-11)
**UI hint**: no

### Phase 13: Scheduler & Admin Toggle
**Goal**: PAX8 sync runs automatically once a day like every other Pulse integration, and can be turned on or off from `/admin/integrations` without a container restart.
**Depends on**: Phase 12
**Requirements**: PAX8-07, PAX8-09
**Success Criteria** (what must be TRUE):
  1. A `pax8-daily` (or equivalently named) entry exists in the sync scheduler and fires once per day, running the full companies + catalog + subscriptions + orders sync in sequence
  2. PAX8 appears as a toggleable row on `/admin/integrations`, backed by the `integration_settings` table like every other integration
  3. Disabling PAX8 from that UI stops future scheduled sync runs (respecting the existing health-cache window, or immediately per the PATCH-clears-cache convention) and records `disabled_by`, `disabled_at`, and an optional `disabled_reason`
  4. Re-enabling PAX8 resumes scheduled sync at the next cron tick with no code deploy or container restart required
**Plans**: 3 plans
- [x] 13-01-PLAN.md — Migration 096 pax8-daily seed + dual-guarded scheduler branch + CLAUDE.md precedent note (PAX8-07, PAX8-09)
- [x] 13-02-PLAN.md — checkConfigOnly('pax8') admin-integrations row + POST /api/pax8/sync 403 disabled-gate (PAX8-09)
- [x] 13-03-PLAN.md — Live verification checkpoint of Phase 13 SC#1-4 (PAX8-07, PAX8-09)
**UI hint**: no

### Phase 14: /pax8 UI Surface
**Goal**: A manager can open `/pax8` and see PAX8 companies with their subscriptions and a cost breakdown, and an admin can resolve any flagged/ambiguous company match directly from that page — no psql required.
**Depends on**: Phase 12
**Requirements**: PAX8-12, PAX8-13, PAX8-14
**Success Criteria** (what must be TRUE):
  1. `/pax8` lists PAX8 companies together with their current subscriptions
  2. Each company shows a cost breakdown (e.g., by subscription/product) built from the synced subscription and order/invoice data
  3. Flagged/ambiguous company matches appear in a distinct, clearly-labeled review section on `/pax8` rather than being mixed silently into the main list
  4. From that review section, an admin can pick the correct Autotask company for a flagged PAX8 company; the resolution persists and is respected (not overwritten) by future syncs
**Plans**: 6 plans
- [x] 14-01-PLAN.md — GET /api/pax8/companies list + /api/pax8/companies/[id] cost-breakdown (requireAuth) (PAX8-13)
- [x] 14-02-PLAN.md — /api/pax8/company-matches queue + admin-gated resolve route + extracted resolver service & test (PAX8-12, PAX8-14)
- [x] 14-03-PLAN.md — DetailModal additive extension: kind prop + PAX8_COMPANY_GROUPS + subscriptions cost-breakdown section (PAX8-13)
- [x] 14-04-PLAN.md — /pax8 page shell + Companies tab (DataTable + DetailModal drill-down) + top-level nav entry (PAX8-13)
- [ ] 14-05-PLAN.md — Needs Review tab (review cards, candidate + manual-search resolve, count badge) + companies-list auth hardening (PAX8-14, PAX8-12)
- [ ] 14-06-PLAN.md — Automated gates + human verification of all 4 SCs and the view/resolve permission split (PAX8-12, PAX8-13, PAX8-14)
**UI hint**: yes

## Progress

**Execution Order:**
Phases execute in numeric order. v1.0 (Phases 1-9.1) shipped 2026-07-10. v2.0 phases run 10 → 11 → 12 → 13 → 14 — each is a hard dependency on the previous within this milestone (no parallelization: auth → current-state sync → historical sync + matching → scheduling → UI, in that order).

| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|
| 1. PWA Scaffolding | v1.0 | 2/2 | Complete | 2026-07-10 |
| 2. Mobile Shell + More Drawer | v1.0 | 2/2 | Complete | 2026-07-10 |
| 3. Dashboard Restyle | v1.0 | 2/2 | Complete | 2026-07-10 |
| 4. Tickets Restyle | v1.0 | 3/3 | Complete | 2026-07-10 |
| 5. Finance Restyle | v1.0 | 2/2 | Complete | 2026-05-03 |
| 6. Analyzer Feed | v1.0 | 3/3 | Complete | 2026-07-10 |
| 7. Engagement Overview | v1.0 | 3/3 | Complete | 2026-07-10 |
| 7.1. User Timezone Fix | v1.0 | 5/5 | Complete | 2026-07-10 |
| 8. Engagement User Profile | v1.0 | 2/2 | Complete | 2026-07-10 |
| 9. User Profile & Preferences | v1.0 | 6/6 | Complete | 2026-07-10 |
| 9.1. ntfy Backend Fix | v1.0 | 1/1 | Complete | 2026-07-10 |
| 10. PAX8 Client & Auth Foundation | v2.0 | 3/3 | Complete    | 2026-07-10 |
| 11. Company, Catalog & Subscription Sync | v2.0 | 3/3 | Complete    | 2026-07-11 |
| 12. Orders/Invoices & Company Matching | v2.0 | 5/5 | Complete    | 2026-07-11 |
| 13. Scheduler & Admin Toggle | v2.0 | 3/3 | Complete    | 2026-07-11 |
| 14. /pax8 UI Surface | v2.0 | 4/6 | In Progress|  |

---
*Roadmap created: 2026-05-03*
*v2.0 phases added: 2026-07-10*
*Source spec (v1.0): `docs/superpowers/specs/2026-05-03-mobile-shell-design.md`*
*Source seed (v2.0): `.planning/seeds/SEED-002-pax8-integration.md`*
</content>
