---
phase: 14-pax8-ui-surface
plan: 04
type: execute
wave: 2
depends_on: ["14-01", "14-03"]
files_modified:
  - app/pax8/page.tsx
  - components/navigation/app-navigation.tsx
autonomous: true
requirements: [PAX8-13]
must_haves:
  truths:
    - "Navigating to /pax8 renders a page with a PageHeader and a Companies / Needs Review Tabs bar, keeping the review section embedded on the same page per D-04 rather than a separate route"
    - "The Companies tab shows a DataTable of PAX8 companies (name, matched Autotask company or Unmatched badge, active subscription count, city/country) with sort/search/pagination, using components/admin/DataTable.tsx per D-01"
    - "Clicking a company row fetches its drill-down and opens the extended DetailModal showing the subscriptions & cost breakdown"
    - "A top-level PAX8 nav entry appears for all authenticated users on desktop and mobile, per D-07's view-is-manager-visible framing"
    - "Page headings and labels use the sitewide 3-weight IBM Plex Sans scale (300-700) approved in D-10, not a trimmed 2-weight variant"
  artifacts:
    - path: "app/pax8/page.tsx"
      provides: "Companies tab (DataTable + DetailModal drill-down) + tab shell with a Needs Review placeholder"
      min_lines: 120
    - path: "components/navigation/app-navigation.tsx"
      provides: "Top-level PAX8 navigationItems entry"
      contains: "'/pax8'"
  key_links:
    - from: "app/pax8/page.tsx"
      to: "/api/pax8/companies"
      via: "fetch in a load function"
      pattern: "fetch\\(`?/api/pax8/companies"
    - from: "app/pax8/page.tsx row click"
      to: "/api/pax8/companies/[id]"
      via: "fetch-then-open DetailModal"
      pattern: "/api/pax8/companies/"
    - from: "app/pax8/page.tsx"
      to: "DetailModal"
      via: "kind='pax8_company'"
      pattern: "kind=\"pax8_company\""
---

<objective>
Create `app/pax8/page.tsx` as a top-level client page: a `PageHeader` + a `Companies` / `Needs Review` `Tabs` shell, with the Companies tab fully implemented (a `DataTable` of PAX8 companies whose row-click fetches the drill-down and opens the extended `DetailModal`). Leave a clearly-marked `Needs Review` `TabsContent` placeholder for Plan 05 to fill. Add the top-level `PAX8` nav entry.

Purpose: PAX8-13 SC#1-2 — `/pax8` lists companies with subscriptions and a per-company cost breakdown. This plan delivers the list + drill-down half and the page skeleton both tabs share. Interface-first: the tab shell and shared state land here so Plan 05 only implements the second tab.
Output: `app/pax8/page.tsx` (Companies tab live, Needs Review stubbed) + nav entry.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/14-pax8-ui-surface/14-RESEARCH.md
@.planning/phases/14-pax8-ui-surface/14-PATTERNS.md
@.planning/phases/14-pax8-ui-surface/14-UI-SPEC.md
@.planning/phases/14-pax8-ui-surface/14-CONTEXT.md
@.planning/phases/14-pax8-ui-surface/14-01-SUMMARY.md

<interfaces>
<!-- Contracts this page consumes. Plans 01 + 03 must be complete. -->

GET /api/pax8/companies?page&limit&sort&order&search (Plan 01) →
  { items: Array<{ id, name, status, city, stateOrProvince, country,
                   autotaskCompanyId, matchConfidence, matchMethod,
                   matchedCompanyName, activeSubscriptionCount }>,
    total, limit, offset }
  Note: route pages by limit/offset — convert the DataTable 1-based `page` to `offset = (page-1)*pageSize`.
  Valid `sort` keys: name | status | city | country | subscriptions | match.

GET /api/pax8/companies/[id] (Plan 01) →
  { company: { id, name, status, city, stateOrProvince, country, website,
               autotaskCompanyId, matchedCompanyName, matchMethod, matchConfidence,
               syncedAt, isDeleted },
    subscriptions: Array<{ subscriptionId, productName, sku, quantity, billingTerm,
                           status, currency, latestBilledAmount, startPeriod }>,
    costTotal }

DetailModal (Plan 03, components/admin/DetailModal.tsx, default export):
  <DetailModal open onOpenChange title data kind /> — pass kind="pax8_company",
  data = { ...company, subscriptions, costTotal }.

DataTable (components/admin/DataTable.tsx, default export) — manual mode:
  columns: Array<{ key, label, sortable?, render?(value,row) }>
  props: data, totalCount, page, pageSize, onPageChange, onSort(col,dir),
         onSearch(q), onRowClick(row), isLoading
  onSort receives the column `key`; map those keys to the API `sort` values above.

PageHeader (components/navigation/page-header.tsx):
  <PageHeader title description breadcrumbs={[{ label: 'PAX8' }]} accent />

Page shell reference: app/engagement/page.tsx (Tabs) + app/admin/data-browser/companies/page.tsx (DataTable+DetailModal composition).

Nav (components/navigation/app-navigation.tsx): navigationItems is a flat array of
  { title, href?, icon?, description?, children? }. The visibleItems filter only special-cases
  titles 'Engagement' and 'Admin' (super-admin only). A top-level entry with any other title is
  visible to all authenticated users — exactly what D-07 requires. Both desktop NavigationMenu and
  mobile-nav.tsx consume this same array. Pick a lucide icon not already used at top level
  (ShoppingCart or CreditCard).
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: /pax8 page shell + Companies tab (DataTable + DetailModal drill-down)</name>
  <read_first>
    - app/engagement/page.tsx (Tabs page-shell pattern: 'use client', container, PageHeader/h1, Tabs/TabsList/TabsTrigger/TabsContent, activeTab state)
    - app/admin/data-browser/companies/page.tsx (DataTable + DetailModal composition: fetch function, columns array, handleRowClick, page/pageSize/totalCount state)
    - components/admin/DataTable.tsx (Column shape + manual-mode props + onSort/onSearch/onRowClick/onPageChange contract)
    - components/admin/DetailModal.tsx (props including the new kind prop from Plan 03)
    - components/navigation/page-header.tsx (PageHeader props)
    - .planning/phases/14-pax8-ui-surface/14-UI-SPEC.md (Copywriting: tab labels 'Companies'/'Needs Review'; Companies empty-state copy; column set; Color: matched-company name as a link, Unmatched badge)
    - .planning/phases/14-pax8-ui-surface/14-01-SUMMARY.md (final response shapes if they differ from the interfaces block)
  </read_first>
  <files>app/pax8/page.tsx</files>
  <action>
    Create a `'use client'` page component `Pax8Page`. Layout: `PageHeader title="PAX8" description="PAX8 companies, subscriptions, and cost breakdown" breadcrumbs={[{ label: 'PAX8' }]} accent` then a `container mx-auto px-6 py-6 space-y-6` wrapper holding a `Tabs` with `TabsList` triggers `Companies` (value `companies`) and `Needs Review` (value `needs-review`), controlled by an `activeTab` state defaulting to `companies`. Implement the Companies `TabsContent` fully: state `companies`, `totalCount`, `page` (1-based), `pageSize` (e.g. 25), `isLoading`, plus `selectedCompany` and `modalOpen`. A `fetchCompanies(page, search?, sort?, order?)` builds a query string translating `page`→`offset=(page-1)*pageSize` and mapping the DataTable column key to the API `sort` value (name/status/city/country/subscriptions/match), fetches `/api/pax8/companies`, sets `companies`/`totalCount`. Define a `columns` array: `name` (sortable), a `matched` column rendering `row.matchedCompanyName` as a primary-colored link-styled span when set else an `<Badge variant="secondary">Unmatched</Badge>` (UI-SPEC Color), `activeSubscriptionCount` (label 'Subs', sortable, font-mono tabular-nums), and a `location` column showing `city, country` (or `stateOrProvince`). `onRowClick` = a `handleRowClick(row)` that FETCHES `/api/pax8/companies/${row.id}` FIRST (fetch-then-open, per PATTERNS.md — keep loading visible on the row, not the dialog), then sets `selectedCompany` to `{ ...resp.company, subscriptions: resp.subscriptions, costTotal: resp.costTotal }` and opens the modal. Render `<DataTable columns data={companies} totalCount page pageSize onPageChange={setPage-then-refetch} onSort onSearch onRowClick isLoading />` with `emptyTitle="No PAX8 companies synced yet"` and `emptyDescription="Run the PAX8 sync from /admin/integrations, then refresh this page."` (UI-SPEC copy). Render `<DetailModal open={modalOpen} onOpenChange={setModalOpen} kind="pax8_company" title={\`PAX8: ${selectedCompany?.name ?? ''}\`} data={selectedCompany} />`. For the `needs-review` `TabsContent`, insert a placeholder comment `{/* Needs Review tab implemented in Plan 14-05 */}` plus a minimal muted "Loading…"/empty node so the tab is not blank — Plan 05 replaces this block. Match error/loading handling to the surrounding codebase (try/catch, console.error, isLoading toggles). No SWR/react-query — useState/useEffect/fetch only (CLAUDE.md).
  </action>
  <verify>
    <automated>npx tsc --noEmit --pretty</automated>
  </verify>
  <acceptance_criteria>
    - `app/pax8/page.tsx` is `'use client'` and renders a `Tabs` with `Companies` and `Needs Review` triggers (grep: both `value="companies"` and `value="needs-review"`)
    - Companies tab fetches `/api/pax8/companies` and renders a `DataTable` with columns including matched-company (link or Unmatched badge) and active subscription count
    - Row click fetches `/api/pax8/companies/${id}` before opening `DetailModal` with `kind="pax8_company"` (grep: `kind="pax8_company"` present)
    - The Needs Review `TabsContent` exists as a marked placeholder (grep: `Plan 14-05` comment) — not blank, not implemented
    - No SWR/react-query import; state via useState/useEffect
    - `npx tsc --noEmit --pretty` passes
  </acceptance_criteria>
  <done>/pax8 renders the tab shell; the Companies tab lists companies and opens a cost-breakdown drill-down; Needs Review is a stub for Plan 05.</done>
</task>

<task type="auto">
  <name>Task 2: Add top-level PAX8 nav entry</name>
  <read_first>
    - components/navigation/app-navigation.tsx (the navigationItems array lines ~50-195 and the visibleItems filter lines ~211-217 — confirm only 'Engagement'/'Admin' are role-filtered)
    - .planning/phases/14-pax8-ui-surface/14-PATTERNS.md (nav section: top-level, non-nested, icon choice)
  </read_first>
  <files>components/navigation/app-navigation.tsx</files>
  <action>
    Add ONE new object to the top-level `navigationItems` array (not inside any `children`): `{ title: 'PAX8', href: '/pax8', icon: <lucide icon>, description: 'PAX8 companies, subscriptions, and cost breakdown' }`. Choose an icon not already used at the top level (e.g. `ShoppingCart` or `CreditCard`) and add it to the existing `lucide-react` import. Place it near the other top-level operational entries (e.g. after `Configuration Items`). Do NOT modify the `visibleItems` filter — leaving `PAX8` out of the `'Engagement' || 'Admin'` special-case means it is visible to all authenticated users (D-07). Do NOT edit `components/navigation/mobile-nav.tsx` — it consumes the same array.
  </action>
  <verify>
    <automated>npx tsc --noEmit --pretty</automated>
  </verify>
  <acceptance_criteria>
    - `navigationItems` contains a top-level entry with `href: '/pax8'` and `title: 'PAX8'` (grep: `'/pax8'` present in app-navigation.tsx)
    - The chosen lucide icon is imported and not previously used at the top level
    - The `visibleItems` filter is unchanged (PAX8 not added to the Engagement/Admin super-admin gate)
    - `npx tsc --noEmit --pretty` passes
  </acceptance_criteria>
  <done>A top-level PAX8 nav item links to /pax8 and is visible to every authenticated user on both desktop and mobile.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| browser → /api/pax8/companies* | Client page fetches list + drill-down data (server routes already gated in Plan 01) |
| DB JSON → React render | Company/subscription strings rendered in DataTable + DetailModal |
| nav visibility | Nav entry visible to all authenticated users by design (D-07); no data leak — the routes themselves enforce auth |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-14-03 | Information Disclosure | /pax8 page data fetches | mitigate | Page relies on Plan 01's `requireAuth()` gates; `/pax8` is a normal (non-public) route so middleware requires a session cookie — do not add `/pax8` to middleware.ts publicRoutes |
| T-14-06 | Tampering (XSS) | DataTable/DetailModal rendering DB strings | mitigate | React text-node auto-escaping; matched-company "link" is a styled span/anchor with no user-controlled href scheme |
| T-14-08 | Information Disclosure | nav entry exposing existence of /pax8 to all roles | accept | D-07 intentionally makes the page manager-visible; the sensitive mutation is separately admin-gated (Plan 02). Nav visibility ≠ data access |
| T-14-SC | Tampering | npm/pip/cargo installs | accept | Zero new packages; only existing components/icons used |
</threat_model>

<verification>
- `npx tsc --noEmit --pretty` passes
- Manual (Plan 06): /pax8 loads for an authenticated user, Companies tab lists real companies, a row opens the cost-breakdown modal, PAX8 appears in the nav
</verification>

<success_criteria>
- Page shell with both tabs; Companies tab fully functional (list + drill-down)
- Needs Review tab is a marked stub for Plan 05
- Top-level PAX8 nav entry visible to all authenticated users
- Type check green
</success_criteria>

<output>
Create `.planning/phases/14-pax8-ui-surface/14-04-SUMMARY.md` when done
</output>
