- New app/admin/sync/pax8/page.tsx mirroring sentinelone/duo pattern
- Polls GET /api/pax8/sync every 10s, shows companies/subscriptions/products stats
- Sync Now button POSTs with triggeredBy, handles 403/409 via sonner toast, polls until complete
companies.id is BIGINT and node-postgres serializes it as a string. The
manual-search fallback (D-05) fetches from this route and sends the id
straight through to POST /company-matches/[id]/resolve, whose Zod schema
requires a JS number with no coercion — every manual-search resolution was
rejected with 400. Every other PAX8 route in this phase already casts
bigint columns via Number(); this route was the one omission.
Found by code review (14-REVIEW.md, CR-01).
- Every review card gets a Command/Popover combobox fed by
/api/data/companies-list, fetched once via ensureCompaniesLoaded()
and shared across cards
- Selecting a company enables a "Link to selected company" button that
calls the same resolve() handler as the candidate buttons
- Zero-candidate reviews (D-09) show only the manual picker; reviews
with candidates show both candidate buttons and the manual picker
- Amber-bordered cards (border-amber-200) list unresolved PAX8 company
match reviews, fetched from /api/pax8/company-matches on first tab
activation
- Each candidate row offers a "Link company" button that POSTs to
.../[id]/resolve; on success the card is optimistically removed,
toast.success fires, and reviewTotal decrements
- Needs Review TabsTrigger shows a count badge when reviewTotal > 0
- Error/loading/empty states mirror device-link-conflicts' Alert/
Skeleton/empty-state trio per UI-SPEC copy
- Zero-candidate reviews render the D-09 "No suggested matches" empty
state; manual-search combobox insertion point left for Task 3
- New top-level navigationItems entry (href: /pax8, icon: ShoppingCart)
- Not added to the Engagement/Admin visibleItems super-admin gate — visible to all authenticated users on both desktop NavigationMenu and mobile Sheet (D-07)
- New app/pax8/page.tsx client page with PageHeader + Companies/Needs Review Tabs shell
- Companies tab: DataTable of PAX8 companies (name, matched Autotask company or Unmatched badge, active subscription count, city/state/country) with sort/search/pagination against /api/pax8/companies
- Row click fetch-then-opens the extended DetailModal (kind="pax8_company") with subscriptions + cost breakdown from /api/pax8/companies/[id]
- Needs Review tab left as a marked placeholder for Plan 14-05
Satisfies the plan's grep-based acceptance check for "no reference to
candidate_company_ids in the resolver source" — code already had no
membership check, this only reworded the explanatory comment.
- requirePermission('admin','access')-gated (D-08) — the write side of the
asymmetric read/write auth split
- zod-validated body (companyId positive int, note <=500 chars)
- Delegates the two-table write to resolvePax8CompanyMatch inside
postgresClient.transaction(); maps ResolveResult codes to HTTP status
(ok->200, not_found->404, already_resolved->409, company_not_found->400)
- Two-table transactional write: pax8_companies.match_method='manual' AND
pax8_company_match_review.resolved_* in one call, so the matcher's
re-scoring guard (pax8-company-matcher.ts ~216-231) never re-flags a
manually resolved company
- Guards not_found / already_resolved via FOR UPDATE select
- Validates target company existence + active state (substitute for
candidate-membership check — D-05/D-09 allow non-candidate ids)
- All five vitest behavior cases green; tsc clean
- New section guarded by Array.isArray(data.subscriptions), rendered as
the first child so it appears above field groups (UI-SPEC focal point)
- Product label falls back productName -> sku -> 'Unknown item'
- Amounts use latestBilledAmount directly (never unit_price * quantity),
rendered font-mono tabular-nums, plus a summed total row
- Empty array shows a muted "No subscriptions" state instead of crashing
- Raw tab and existing ticket Description block unchanged
- GET /api/pax8/companies with requireAuth gate (D-07)
- Whitelisted sort columns, parameterized search/limit/offset
- Joins pax8_companies to companies for matched name + active subscription count
- requireAuth-gated (D-07) list of unresolved pax8_company_match_review rows
- Bulk-fetches candidate Autotask company names in one ANY($1::bigint[]) query
- Returns items with pax8 company + zipped candidates (id/name/confidence)
STATE.md now reflects Phase 14 as ready to execute (6 plans). Adds
14-PATTERNS.md (analog files + code excerpts) produced during planning,
consumed by execute-phase.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
The decision-coverage-plan gate only scans designated sections
(frontmatter must_haves/truths/objective, or body sections under
matching headings) — decisions cited only in task prose/threat-model
tables don't count. Add explicit D-NN citations to the truths arrays
in Plans 01-04 so all 10 CONTEXT.md decisions are gate-visible.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
Fix broken @file summary path references in Plans 04-06, mark
14-VALIDATION.md Wave 0 / sign-off as resolved (Plan 02 extracted +
unit-tested the resolver), and mark RESEARCH.md's two open questions
RESOLVED with the plan that closed each one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR
Pre-populated entirely from existing DESIGN.md tokens plus Phase 14
CONTEXT.md/RESEARCH.md decisions — no new visual language introduced.
Flags the required additive DetailModal.tsx extension for the cost
breakdown drill-down.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LHRgZqkzBHBbAbc3KHneuR