# Phase 14: /pax8 UI Surface - Context

**Gathered:** 2026-07-11
**Status:** Ready for planning

<domain>
## Phase Boundary

A new `/pax8` page where any authenticated manager can see PAX8 companies with
their subscriptions and a cost breakdown, and an admin can resolve flagged or
ambiguous company matches (populated by Phase 12's fuzzy matcher) directly
from that same page — no `psql` required. No new sync logic (Phases 11-12
already built and schedule it — Phase 13), no changes to the matching
algorithm itself, no PAX8 write access (read-only view + resolve-to-Autotask
metadata only).

</domain>

<decisions>
## Implementation Decisions

User deferred to Claude's judgment on all four gray areas identified. Decisions below are grounded in existing Pulse conventions and Phase 12/13 schema, documented as concrete direction for research/planning — not open questions.

### Company List & Cost Breakdown Layout
- **D-01:** Main company list uses `components/admin/DataTable.tsx` (the
  established `@tanstack/react-table` wrapper used throughout Pulse) rather
  than a bespoke card grid — consistent with every other tabular list in the
  app, gives sorting/search/pagination for free.
- **D-02:** Per-company cost breakdown is a drill-down via
  `components/admin/DetailModal.tsx` (formatted/raw tab pattern), not inline
  in the table row — keeps the main list scannable. The formatted tab shows
  subscriptions (product name, quantity, billing term) and a cost summary;
  raw tab shows the underlying JSON for support/debugging, matching every
  other DetailModal usage in the codebase.
- **D-03:** Cost breakdown is grouped **by subscription/product**, not a
  single total — matches `pax8_subscriptions`/`pax8_order_items` granularity
  and gives managers the "what am I paying for" answer, not just "how much."
  Note the schema quirk from Phase 12 research: `pax8_orders.pax8_company_id`
  is always NULL (PAX8's `/invoices` API never populates it) — actual
  per-company cost data must be joined from `pax8_order_items.pax8_company_id`
  and `start_period`/`end_period`, not from `pax8_orders` directly.

### Review/Resolution Section & Flow
- **D-04:** The flagged/ambiguous review queue is a **section/tab within
  `/pax8`** (e.g., "Companies" / "Needs Review" tabs on the same page), not a
  separate top-level route — matches the roadmap's explicit "directly from
  that page" wording (SC#4), even though the closest existing precedent
  (`app/admin/device-link-conflicts/page.tsx`) is a standalone page. Reuse
  that page's card-list-with-resolve-action UI pattern, just embedded as a
  tab rather than routed separately.
- **D-05:** Resolution UI offers **both**: pick from the top-3 stored
  candidates (`pax8_company_match_review.candidate_company_ids` +
  `match_confidences`, already computed by Phase 12 — zero extra query cost)
  as the primary path, with a manual company search/picker as a fallback —
  required regardless for Phase 12's D-03 zero-candidate case (empty
  `candidate_company_ids` array), and useful even when candidates exist but
  none are correct.
- **D-06:** Resolving writes `resolved_at`, `resolved_by_user_id`,
  `resolved_to_company_id`, optional `resolution_note` — mirrors
  `device_link_review`'s resolve shape exactly (same columns exist on
  `pax8_company_match_review` per Phase 12's schema). This is what makes the
  resolution "persist and be respected by future syncs" (SC#4) — Phase 12's
  D-05 already scopes re-matching to `resolved_at IS NULL` rows only.

### Access & Permissions Split
- **D-07:** Viewing `/pax8` (company list + cost breakdown) requires only
  `requireAuth()` — any authenticated user, matching the roadmap's "a manager
  can open /pax8" framing and how other read-heavy pages (dashboard, tickets)
  work today.
- **D-08:** The resolve action (mutating a match) requires
  `requirePermission('admin', 'access')` — matching
  `/api/admin/device-link-conflicts/[id]/resolve`'s existing pattern exactly.
  This intentionally differs from that precedent's GET route (which is
  admin-gated end-to-end) because Phase 14's roadmap explicitly splits
  "manager views" from "admin resolves" — the read/write permission split is
  a deliberate phase-14 decision, not an oversight.
- **Note for the planner:** Phase 13's code review (`13-REVIEW.md`, CR-02)
  flagged that the adjacent `POST /api/pax8/sync` route has no role check at
  all — a pre-existing gap, out of Phase 13's scope. Phase 14 should NOT
  repeat that gap: every new API route this phase adds must have an explicit
  `requireAuth()`/`requirePermission()` call, not an implicit "whatever
  middleware allows."

### Typography Exception (user-approved after UI-checker BLOCK)
- **D-10:** The UI checker's Dimension 4 gate blocked Phase 14's first
  UI-SPEC draft for using 3 font weights (400/600/700) against its generic
  2-weight default cap. Presented to the user with the tradeoff (match
  `DESIGN.md`'s existing sitewide type scale — IBM Plex Sans in weights
  300-700, already used on every other page — vs. trim to 2 weights for
  `/pax8` alone, which would look inconsistent with the rest of the app).
  User explicitly chose to match the sitewide 3-weight pattern. This is a
  real recorded decision, not Claude's discretion.

### No-Candidate / Edge-Case Handling
- **D-09:** A review row with an empty `candidate_company_ids` array shows
  the same review-section UI with an explicit empty state ("No suggested
  matches — search manually") rather than a separate flow or page — the
  manual-search fallback from D-05 handles this case naturally, no special-
  casing needed beyond the empty-state copy.

### Claude's Discretion
- Exact tab/section labels, table column set, and DetailModal field layout —
  left to the planner/researcher; no specific mockup or wording was given.
- Whether "Needs Review" tab shows a count badge, and sort/filter options on
  the main company list — standard implementation details, not discussed.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Project scope & requirements
- `.planning/PROJECT.md` — Current Milestone: v2.0 PAX8 Integration section
- `.planning/REQUIREMENTS.md` — PAX8-12, PAX8-13, PAX8-14 (this phase's
  requirement IDs)
- `.planning/ROADMAP.md` — Phase 14 section (goal, 4 success criteria,
  depends on Phase 12)

### Prior phase foundation this phase builds on
- `migrations/091_pax8_tables.sql` — `pax8_companies`, `pax8_products`,
  `pax8_subscriptions`, `pax8_orders`, `pax8_order_items`,
  `pax8_company_match_review` — full schema this page reads from
- `migrations/093_pax8_orders_company_matching.sql` — `pax8_companies`
  auto-match columns (`autotask_company_id`, `match_confidence`,
  `match_method`, `matched_at`) and the critical note that
  `pax8_orders.pax8_company_id` is always NULL — cost-per-company data comes
  from `pax8_order_items` instead
- `.planning/phases/12-orders-invoices-company-matching/12-CONTEXT.md` —
  D-01 through D-05 (match confidence, ambiguous handling, empty-candidate
  flagging, top-3 candidate cap, re-scoring rules) — the review queue this
  phase surfaces
- `.planning/phases/13-scheduler-admin-toggle/13-REVIEW.md` — CR-02 finding
  (missing permission check on `/api/pax8/sync`) — this phase's new routes
  must not repeat that gap (see D-08 note above)

### Existing patterns to follow
- `app/admin/device-link-conflicts/page.tsx` — direct precedent for the
  review/resolve UI (list + resolve action, card-based); this phase embeds
  the same pattern as a tab/section rather than a standalone route
- `app/api/admin/device-link-conflicts/route.ts` and
  `app/api/admin/device-link-conflicts/[id]/resolve/route.ts` — API shape
  (list with filters, resolve with `requirePermission('admin', 'access')`)
  to mirror for `pax8_company_match_review`
- `components/admin/DataTable.tsx` — table wrapper for the main company list
- `components/admin/DetailModal.tsx` — formatted/raw tab pattern for the
  per-company cost breakdown drill-down
- `lib/auth-utils.ts` — `requireAuth()` / `requirePermission()` helpers this
  phase's routes must use explicitly (D-07/D-08)

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `components/admin/DataTable.tsx` — sortable/searchable/paginated table,
  used as-is for the company list
- `components/admin/DetailModal.tsx` — formatted/raw tabs, used as-is for
  the cost-breakdown drill-down
- `postgresClient` singleton — parameterized queries, no ORM, joins across
  `pax8_companies` / `pax8_subscriptions` / `pax8_order_items` /
  `pax8_company_match_review`

### Established Patterns
- Admin CRUD pages live at `app/admin/<name>/page.tsx` with a matching
  `app/api/admin/<name>/route.ts` — but this page is NOT admin-only
  (D-07/D-08 split), so its route likely lives at `app/pax8/page.tsx` (top
  level, alongside `/dashboard`, `/mobile`), not under `/admin`
- `*_match_review` tables (both `device_link_review` and
  `pax8_company_match_review`) follow list-with-filter + single-item-resolve
  API shape: `GET /api/.../conflicts?source=X` + `POST /api/.../[id]/resolve`
- Toasts via `sonner`, icons via `lucide-react` — standard for the resolve
  action's success/error feedback

### Integration Points
- New `app/pax8/page.tsx` — main page with Companies / Needs Review tabs
- New API routes for: company list + cost data, match-review list, and
  resolve action (exact paths left to planner — likely
  `app/api/pax8/companies/route.ts`,
  `app/api/pax8/company-matches/route.ts` +
  `app/api/pax8/company-matches/[id]/resolve/route.ts`)
- `components/navigation/app-navigation.tsx` — needs a new top-level nav
  entry for `/pax8` (not nested under Admin, given D-07's broader view
  access)

</code_context>

<specifics>
## Specific Ideas

No UI mockups or specific behavioral scripts were provided — the nine
numbered decisions above (D-01 through D-09) are the concrete specifics:
DataTable + DetailModal-drill-down layout, embedded review tab (not a
separate page) with dual pick-from-candidates/search resolution, an explicit
manager-view/admin-resolve permission split, and empty-state handling for
the zero-candidate case.

</specifics>

<deferred>
## Deferred Ideas

None — discussion stayed within Phase 14's scope. (PAX8 write access,
seat-adjustment actions, and any general natural-language assistant over
this data are already explicitly out of scope per PROJECT.md, not deferred
from this discussion.)

</deferred>

---

*Phase: 14-pax8-ui-surface*
*Context gathered: 2026-07-11*
