---
phase: quick-260712-ash
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - app/admin/sync/pax8/page.tsx
  - app/admin/sync/page.tsx
  - public/logos/pax8.ico
autonomous: true
requirements: [QUICK-PAX8-SYNC-UI]

must_haves:
  truths:
    - "A PAX8 integration card appears on /admin/sync alongside the other integrations"
    - "The PAX8 card shows last sync time plus company and subscription counts"
    - "Clicking the PAX8 card navigates to /admin/sync/pax8"
    - "The PAX8 detail page shows sync status, counts, and sync history"
    - "The detail page 'Sync Now' button POSTs to /api/pax8/sync and shows loading + toast feedback"
  artifacts:
    - path: "app/admin/sync/pax8/page.tsx"
      provides: "PAX8 sync detail page with Sync Now button"
      min_lines: 100
    - path: "app/admin/sync/page.tsx"
      provides: "PAX8 entry in INTEGRATIONS array + summary/stats/icon logic + fetch wiring"
      contains: "id: 'pax8'"
    - path: "public/logos/pax8.ico"
      provides: "PAX8 card logo asset"
  key_links:
    - from: "app/admin/sync/page.tsx"
      to: "/api/pax8/sync"
      via: "fetch in fetchAll"
      pattern: "fetch\\('/api/pax8/sync'\\)"
    - from: "app/admin/sync/pax8/page.tsx"
      to: "/api/pax8/sync"
      via: "GET for status + POST for Sync Now"
      pattern: "/api/pax8/sync"
---

<objective>
Add PAX8 to the admin sync overview page (`/admin/sync`) as an integration card, and give it a dedicated detail page at `/admin/sync/pax8` — matching the existing duo/sentinelone pattern. The card shows last sync time plus company/subscription counts; the detail page shows sync status, counts, history, and a manual "Sync Now" button that POSTs to the existing `/api/pax8/sync` route.

Purpose: PAX8 already syncs on a daily schedule (`pax8-daily`) and exposes `/api/pax8/sync`, but has no operator-facing surface in the sync admin area. This closes that gap so a manager can see PAX8 sync state and trigger a manual sync from the same place as every other integration.

Output: A new detail page, an updated overview page, and a PAX8 logo asset.
</objective>

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

<context>
@./CLAUDE.md

<interfaces>
<!-- The existing /api/pax8/sync route (both verbs) is the single data source. -->
<!-- Executor should use these shapes directly — no codebase exploration needed. -->

GET /api/pax8/sync returns:
```
{
  inProgress: boolean,
  counts: { companies: number, subscriptions: number, products: number },
  history: Array<{
    id: string,
    sync_type: string,
    status: string,            // 'completed' | 'running' | 'failed' (and possibly others)
    started_at: string,
    completed_at: string | null,
    records_added: number,
    records_updated: number,
    records_deleted: number,
    error_message: string | null,
    triggered_by: string
  }>
}
```
NOTE: unlike the SentinelOne route, this history has NO `duration_ms` and NO
`total_upserted` / `entity_results`. Derive total records as
`records_added + records_updated + records_deleted`, and compute duration as
`new Date(completed_at) - new Date(started_at)` when both are present.

POST /api/pax8/sync — fire-and-forget. Body `{ triggeredBy: 'manual' }`.
Response codes to handle:
  - 200 `{ ok: true, message }` — sync started
  - 403 `{ error, message }` — PAX8 disabled via /admin/integrations
  - 409 `{ error }` — sync already in progress
</interfaces>

<!-- Reference implementations to mirror — do NOT modify these: -->
@app/admin/sync/sentinelone/page.tsx
@app/admin/sync/duo/page.tsx
@app/admin/sync/page.tsx
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create the PAX8 sync detail page</name>
  <files>app/admin/sync/pax8/page.tsx</files>
  <action>
Create a `'use client'` detail page mirroring `app/admin/sync/sentinelone/page.tsx`'s structure (back link + logo/title header, stat cards, last-sync card, history list, Sync Now button), adapted to the PAX8 `/api/pax8/sync` shape from the interfaces block.

Specifics:
- Fetch status from `GET /api/pax8/sync` in a `fetchData` callback; poll every 10s via `setInterval` (match sentinelone). Use `useUserTimezone()` from `@/lib/hooks/use-user-timezone` for date formatting.
- Header: back link to `/admin/sync`, `<img src="/logos/pax8.ico" .../>`, title "PAX8 Sync", subtitle describing "Companies, subscriptions, and products synced to pax8_* tables".
- Stat cards (shadcn Card): Companies, Subscriptions, Products — read from `data.counts`. Use `lucide-react` icons (e.g. Building2, Package, Boxes) and `Number(value ?? 0).toLocaleString()`.
- Last-sync card: read `data.history[0]`. Show status icon (completed → CheckCircle2 green, running → RefreshCw spinning blue, else XCircle red), the formatted `completed_at || started_at`, computed duration, and total records = `records_added + records_updated + records_deleted`. Render `error_message` in a red box when present.
- History list: iterate `data.history`, each row showing status icon, `started_at` (formatted), total records, computed duration, and a Badge with `triggered_by`. Show an empty-state line when history is empty and not loading.
- Sync Now button: `triggerSync` sets `syncing`, calls `POST /api/pax8/sync` with header `Content-Type: application/json` and body `{ triggeredBy: 'manual' }`. On non-ok response, read the JSON and `toast.error(json.message || json.error)` (import `toast` from `sonner`) — this covers the 403 disabled and 409 in-progress cases. On ok, `toast.success('PAX8 sync started')` and poll `GET /api/pax8/sync` every 5s (like duo's poll) until `!inProgress`, then clear `syncing` and `fetchData()`. Disable the button while `syncing` or `data.inProgress`.
Do NOT use any state library — plain `useState`/`useEffect`/`useCallback` + `fetch`, matching the sibling pages.
  </action>
  <verify>
    <automated>test -f /opt/stacks/pulse/app/admin/sync/pax8/page.tsx && grep -q "'/api/pax8/sync'" /opt/stacks/pulse/app/admin/sync/pax8/page.tsx && grep -q "triggeredBy" /opt/stacks/pulse/app/admin/sync/pax8/page.tsx && npx --prefix /opt/stacks/pulse tsc --noEmit --pretty -p /opt/stacks/pulse/tsconfig.json</automated>
  </verify>
  <done>Detail page exists, fetches GET /api/pax8/sync, has a Sync Now button POSTing with triggeredBy, handles 403/409 via sonner toast, and type-checks clean.</done>
</task>

<task type="auto">
  <name>Task 2: Add the PAX8 card + logo to the sync overview page</name>
  <files>app/admin/sync/page.tsx, public/logos/pax8.ico</files>
  <action>
Add a PAX8 logo asset and wire a PAX8 card into `app/admin/sync/page.tsx`, matching the existing per-integration pattern (INTEGRATIONS entry + fetch in `fetchAll` + `getSummary` branch + `getStatusIcon` branch + a stats block in the card render).

Logo: create `public/logos/pax8.ico`. Fetch the PAX8 favicon (`curl -fsSL https://www.pax8.com/favicon.ico -o /opt/stacks/pulse/public/logos/pax8.ico`). If the fetch fails or returns empty, copy an existing logo as a placeholder (`cp /opt/stacks/pulse/public/logos/itglue.ico /opt/stacks/pulse/public/logos/pax8.ico`) so the card never shows a broken image — note in the commit that a real PAX8 logo should replace the placeholder.

Overview page edits (`app/admin/sync/page.tsx`):
- Append to `INTEGRATIONS`: `{ id: 'pax8', category: 'Licensing', product: 'PAX8', description: 'Companies, subscriptions, products, and license billing', href: '/admin/sync/pax8', logo: '/logos/pax8.ico', color: 'blue' }`.
- Add state `const [pax8Data, setPax8Data] = useState<any>(null);` and add `fetch('/api/pax8/sync')` to the `Promise.all` in `fetchAll`, wiring its result via `if (pax8Res.ok) setPax8Data(await pax8Res.json());`.
- Add a `pax8` branch to `getSummary`: return `{ lastSync: pax8Data?.history?.[0]?.completed_at ?? null, status: pax8Data?.history?.[0]?.status ?? null, companies: Number(pax8Data?.counts?.companies ?? 0), subscriptions: Number(pax8Data?.counts?.subscriptions ?? 0) }` (guard on `pax8Data` returning null when absent).
- Add a `pax8` branch to `getStatusIcon`: no `lastSync` → Clock muted; `status === 'failed'` → XCircle red; else CheckCircle2 green.
- Add a `{intg.id === 'pax8' && summary && (...)}` stats block in the card body showing "Last sync" (via `fmtDate`), "Companies", and "Subscriptions" rows — matching the existing itglue/duo stat-row markup.
Match existing formatting/indentation. Do not alter other integrations' logic.
  </action>
  <verify>
    <automated>test -s /opt/stacks/pulse/public/logos/pax8.ico && grep -q "id: 'pax8'" /opt/stacks/pulse/app/admin/sync/page.tsx && grep -q "fetch('/api/pax8/sync')" /opt/stacks/pulse/app/admin/sync/page.tsx && grep -q "setPax8Data" /opt/stacks/pulse/app/admin/sync/page.tsx && npx --prefix /opt/stacks/pulse tsc --noEmit --pretty -p /opt/stacks/pulse/tsconfig.json</automated>
  </verify>
  <done>PAX8 card renders on /admin/sync with last sync + company/subscription counts, links to /admin/sync/pax8, a non-empty pax8.ico exists, and the page type-checks clean.</done>
</task>

</tasks>

<verification>
- `npx tsc --noEmit --pretty` passes (no new type errors).
- Manual smoke (developer): visit `/admin/sync` — PAX8 card is present with counts and last-sync; click it → lands on `/admin/sync/pax8`; click "Sync Now" → button shows a spinner, a sonner toast appears, and (if PAX8 is disabled in /admin/integrations) an error toast surfaces instead.
</verification>

<success_criteria>
- PAX8 appears as an integration card on `/admin/sync` showing last sync time + company/subscription counts.
- The card links to a working `/admin/sync/pax8` detail page.
- The detail page shows counts, last-sync status, history, and a functional "Sync Now" button POSTing to `/api/pax8/sync` with loading + toast feedback.
- No new dependencies or state libraries introduced; conventions (`'use client'`, shadcn, lucide, sonner, plain fetch) followed.
- Type check clean.
</success_criteria>

<output>
Create `.planning/quick/260712-ash-add-pax8-to-the-admin-sync-overview-page/260712-ash-SUMMARY.md` when done.
</output>
