33 KiB
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 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.
- 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/analyzerread-only stream +/api/mobile/analyzer/feed - Phase 7: Engagement Overview (NEW) —
/mobile/engagementphone-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/profilesettings 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):
- Visiting
/manifest.jsonreturns valid JSON withname: "Pulse",display: "standalone",start_url: "/mobile", and theme/background colors matching the app shells - The root
app/layout.tsxreferences the manifest via<link rel="manifest">and the viewport meta includesviewport-fit=cover - 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 - 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):
- 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 - 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-primarybased onpathname.startsWith(href) - 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
ExternalLinkhint), and Account (current user read-only + Sign out) - Tapping Sign out in the drawer signs the user out and lands them on
/auth/sign-in app/mobile/nav/page.tsxno longer exists; visiting/mobile/navdoes not render the old standalone nav page- 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):
- Dashboard renders a 2×2 grid of four primary KPI cards drawn from desktop hero stats (no 1×4 row, no charts)
- Below the grid, a "Needs Attention" horizontally-scrollable strip surfaces overdue tickets, failed backups, and stalled workflows; tapping a card opens its detail view
- A compact status row shows analyzer worker, RMM worker, and backup-success-rate; tapping any element opens the corresponding desktop admin page
- 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):
- 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)
- 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
- Single-tapping a row navigates to
/mobile/tickets/[id] - 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
- 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):
/mobile/financeadopts the new Card and typography scale — no horizontal overflow, spacing legible on small phones- 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):
- Tapping the Analyzer tab in the bottom nav lands on
/mobile/analyzerand shows a most-recent-first list of AI ticket analyses - Each row shows ticket #, title, the analyzer's one-line summary, a confidence badge, and a stage indicator (Triage → Analyze → Deep Review)
- 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
- The mobile feed never exposes editing, re-run, or prompt-tuning controls (read-only by design)
- The list reads from
analyzer_analysesvia/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):
- The Mobile sections row in the More drawer links to
/mobile/engagement; the Analyzer is on the bottom bar but Engagement is not - The overview page shows a period selector (today / 7d / 30d) sticky just below the H1, with active period clearly indicated
- 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
- 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)
- 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):
- 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 - 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)
- Authenticated
GET /api/me/timezonereturns the user's tz;PUT /api/me/timezoneaccepts an IANA string and rejects anything not inIntl.supportedValuesOf('timeZone') - A shared client hook (
useUserTimezone()) reads the value fromuseSession()so all components use a single source of truth — no per-pageIntlcalls scattered around - 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):
- Tapping a row in the per-employee list navigates to
/mobile/engagement/[userId](segment form, shareable URL) - The profile is a real page (not a modal) — the device/browser back gesture returns to the overview at the same scroll position
- 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 addsroute_to_user)lib/auth.tsadditionalFields(Phase 7.1 precedent forthemecolumn exposure)migrations/033_create_pipeline_engine_tables.sql(notification_channels,pipeline_stepsshapes)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):- 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 byrequireAuth()and saving per-user (PROF-01..04) - Theme persists server-side and applies on sign-in across devices via the
themecolumn onuser(Better Auth additionalFields), with next-themes still handling FOUC and the desktopThemeTogglewriting through to the server (THEME-01..05) - 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) - 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) lib/services/pipeline-steps/notify.tshonors an optionalroute_to_userblock 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'schannel_idon no-channel/send-failure (recorded asuser_route_fallback) but skipping silently when the user has the toggle muted (ROUTE-01..07) Plans: 6 plans
- Tapping Profile/Account in the More drawer routes to
- 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):
mintNtfyTopic()returnspulse-me-XXXXXXXX;NTFY_TOPIC_REenforces^pulse-me-[A-Za-z0-9-]{6,64}$; custom topics matchingpulse-,noc-,soc-, or arbitrary names are rejected- All four ntfy publish paths used for personal channels (
sendChannelTest,pipeline-steps/notify.ts sendNtfy,pipeline-steps/approval.tsntfy branch,ticket-digest-service.ts deliver()ntfy branch) target${NTFY_BASE_URL || 'https://ntfy.wulfconsulting.cloud'}and sendAuthorization: Bearer ${NTFY_PULSE_TOKEN}whenchannel.owner_user_idis set - Global / admin ntfy channels (
owner_user_id IS NULL) preserve their existingchannel.config.server_url/channel.config.auth_tokenbehavior — out-of-scope per gap diagnosis /mobile/profileQR code and subscribe link targetprocess.env.NEXT_PUBLIC_NTFY_BASE_URL || 'https://ntfy.wulfconsulting.cloud'; help line under custom-topic Input reads "Topic must start withpulse-me-"npx tsc --noEmit --prettyandnpx vitest run lib/services/pipeline-steps/notify.test.tsboth 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 (completed 2026-07-11)
- 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)
- Phase 13: Scheduler & Admin Toggle — Daily
pax8-dailycron 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):
lib/services/pax8-factory.tsexportsisPax8Configured(), returningtrueonly when the PAX8 client ID and secret env vars are both set,falseotherwisegetPax8Client()performs an OAuth2 client-credentials token exchange againstapi.pax8.com/v1and successfully calls a read-only endpoint (e.g., list companies) using the resulting bearer token- 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 - 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):
- Running the sync populates a companies table with every PAX8 company (PAX8 ID, name, and other identifying fields)
- Running the sync populates a product/catalog table (SKUs, categories) and a subscriptions table (product, seat count, billing term) per company
- A synced subscription row displays a readable product name and category by joining to the catalog table — not a bare SKU/product ID
- 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):
- Running the sync populates an orders/invoices table with historical line items (not just current-state seat counts), enabling cost-over-time comparisons
- 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
- 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
- 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
- 12-01-PLAN.md — Migration 093 (pg_trgm + pax8_order_items/pax8_companies columns) + Pax8Invoice/Pax8InvoiceItem types (PAX8-06, PAX8-10, PAX8-11)
- 12-02-PLAN.md — pax8-client listAllInvoices/listAllInvoiceItems + tests + live field-mapping spot-check (PAX8-06)
- 12-03-PLAN.md — pax8-company-matcher.ts (pg_trgm similarity, 0.90 threshold, tie/empty/idempotency policy) + tests (PAX8-10, PAX8-11)
- 12-04-PLAN.md — syncOrders + syncCompanyMatches wired into Pax8SyncService.fullSync + sync-service tests (PAX8-06, PAX8-10, PAX8-11)
- 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):
- 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 - PAX8 appears as a toggleable row on
/admin/integrations, backed by theintegration_settingstable like every other integration - 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 optionaldisabled_reason - Re-enabling PAX8 resumes scheduled sync at the next cron tick with no code deploy or container restart required Plans: 3 plans
- 13-01-PLAN.md — Migration 096 pax8-daily seed + dual-guarded scheduler branch + CLAUDE.md precedent note (PAX8-07, PAX8-09)
- 13-02-PLAN.md — checkConfigOnly('pax8') admin-integrations row + POST /api/pax8/sync 403 disabled-gate (PAX8-09)
- 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):
/pax8lists PAX8 companies together with their current subscriptions- Each company shows a cost breakdown (e.g., by subscription/product) built from the synced subscription and order/invoice data
- Flagged/ambiguous company matches appear in a distinct, clearly-labeled review section on
/pax8rather than being mixed silently into the main list - 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
- 14-01-PLAN.md — GET /api/pax8/companies list + /api/pax8/companies/[id] cost-breakdown (requireAuth) (PAX8-13)
- 14-02-PLAN.md — /api/pax8/company-matches queue + admin-gated resolve route + extracted resolver service & test (PAX8-12, PAX8-14)
- 14-03-PLAN.md — DetailModal additive extension: kind prop + PAX8_COMPANY_GROUPS + subscriptions cost-breakdown section (PAX8-13)
- 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 | 5/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