wulf-pulse/.planning/ROADMAP.md
lorentz c8dd64f766 docs(11): create phase plan
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
2026-07-10 19:04:29 -04:00

31 KiB
Raw Blame History

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.

v1.0 Mobile Shell Redesign (Phases 1-9.1) - SHIPPED 2026-07-10

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 37 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.

  • Phase 1: PWA Scaffolding — Manifest, viewport meta, and safe-area utilities so the shell installs and paints under the home indicator
  • Phase 2: Mobile Shell + More Drawer — New app/mobile/layout.tsx (header + 5-cell bottom nav) and Sheet drawer that replaces /mobile/nav
  • Phase 3: Dashboard Restyle — 2×2 KPI grid, Needs Attention strip, worker/backup status row (no charts)
  • Phase 4: Tickets Restyle — Collapsible URL-synced filters, priority-bar rows, cursor-based infinite scroll, detail header reskin
  • Phase 5: Finance Restyle — Adopt new Card + typography scale, swap wide tables for stacked lists (completed 2026-05-03)
  • Phase 6: Analyzer Feed (NEW)/mobile/analyzer read-only stream + /api/mobile/analyzer/feed
  • Phase 7: Engagement Overview (NEW)/mobile/engagement phone-first overview reachable from the More drawer
  • 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"
  • Phase 8: Engagement User Profile (NEW)/mobile/engagement/[userId] real-page profile that replaces the desktop modal pattern
  • Phase 9: User Profile & Preferences (NEW)/mobile/profile settings page (timezone chooser, theme, mobile push, Teams + ntfy channels)
  • 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
  • 01-01-PLAN.md — Web App Manifest + viewport-fit=cover (PWA-01, PWA-02, PWA-03)
  • 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
  • 02-01-PLAN.md — Build mobile shell components (HeaderBar, BottomNav, MoreDrawer) + analyzer placeholder (SHELL-02..04, SHELL-06, NAV-01..03, DRAWER-01..05)
  • 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
  • 03-01-PLAN.md — /api/mobile/dashboard reshape + KpiCardMobile/NeedsAttentionStrip/WorkerStatusRow components (DASH-01, DASH-02, DASH-03)
  • 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
  • 04-01-PLAN.md — /api/mobile/tickets cursor rewrite + TicketFilterStrip + TicketRowSkeleton components (TICK-01, TICK-02, TICK-05)
  • 04-02-PLAN.md — Replace app/mobile/tickets/page.tsx with URL-synced filters, priority-stripe rows, IntersectionObserver infinite scroll (TICK-01..TICK-06)
  • 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
  • 05-01-PLAN.md — FinanceRow + FinanceSkeleton helper components (FIN-01, FIN-02)
  • 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
  • 06-01-PLAN.md — /api/mobile/analyzer/feed endpoint with cursor pagination + kiosk_settings scoping (ANL-01, ANL-02, ANL-06)
  • 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)
  • 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
  • 07-01-PLAN.md — /api/mobile/engagement/summary + /api/mobile/engagement/trend endpoints with period whitelist + requireAuth (ENG-03, ENG-05)
  • 07-02-PLAN.md — Engagement* mobile components (PeriodChips, SummaryCard, HoursSparkline, SortChips, SearchInput, UserRow, UserRowSkeleton + getInitials utility) (ENG-02, ENG-03, ENG-04, ENG-05)
  • 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
  • 07.1-01-PLAN.md — Add timezone column to user table + Better Auth additionalField (TZ-01)
  • 07.1-02-PLAN.md — /api/me/timezone GET + PUT with IANA validation (TZ-03)
  • 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)
  • 07.1-04-PLAN.md — useUserTimezone() client hook + reported-bug-surface mobile page migration + codebase-wide audit (TZ-04, TZ-02 client portion)
  • 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
  • 08-01-PLAN.md — MS Graph user-photo proxy at /api/mobile/engagement/user/[userId]/photo (ENG-06; D-25, D-26)
  • 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
  • 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)
  • 09-02-PLAN.md — /api/me/* endpoints: theme, channels (Teams + ntfy), notification-subscriptions matrix (THEME-02, CHAN-02..05, CHAN-07, SUB-04)
  • 09-03-PLAN.md — notify.ts route_to_user branch + resolver registry + fallback semantics (ROUTE-01..06)
  • 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)
  • 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)
  • 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
  • 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)

📋 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.

  • Phase 10: PAX8 Client & Auth Foundation — OAuth2 client-credentials auth, isPax8Configured(), and the PAX8 schema migration (completed 2026-07-10)
  • Phase 11: Company, Catalog & Subscription Sync — Read-only sync of current-state companies, product catalog, and subscriptions into Postgres
  • Phase 12: Orders/Invoices & Company Matching — Historical cost sync plus fuzzy-name auto-matching (with flagging) of PAX8 companies to Autotask companies
  • Phase 13: Scheduler & Admin Toggle — Daily pax8-daily cron entry and an on/off switch in /admin/integrations
  • 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
  • 10-01-PLAN.md — PAX8 types + OAuth2 client (token exchange, audience, cache) + factory (isPax8Configured/getPax8Client) + mocked tests (PAX8-01, PAX8-02)
  • 10-02-PLAN.md — migrations/091_pax8_tables.sql (6 PAX8 tables, IF NOT EXISTS) + apply to dev DB (PAX8-01, PAX8-02)
  • 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
  • 11-01-PLAN.md — Migration 092 subscription cost columns + extend pax8 types + read-only client pagination helpers (PAX8-04, PAX8-05, PAX8-08)
  • 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)
  • 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: TBD 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: TBD 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: TBD 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 0/TBD Not started -
12. Orders/Invoices & Company Matching v2.0 0/TBD Not started -
13. Scheduler & Admin Toggle v2.0 0/TBD Not started -
14. /pax8 UI Surface v2.0 0/TBD Not started -

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