diff --git a/.planning/quick/260712-ash-add-pax8-to-the-admin-sync-overview-page/260712-ash-PLAN.md b/.planning/quick/260712-ash-add-pax8-to-the-admin-sync-overview-page/260712-ash-PLAN.md new file mode 100644 index 0000000..c8e1998 --- /dev/null +++ b/.planning/quick/260712-ash-add-pax8-to-the-admin-sync-overview-page/260712-ash-PLAN.md @@ -0,0 +1,159 @@ +--- +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" +--- + + +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. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md + + + +@./CLAUDE.md + + + + + +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 + + + +@app/admin/sync/sentinelone/page.tsx +@app/admin/sync/duo/page.tsx +@app/admin/sync/page.tsx + + + + + + Task 1: Create the PAX8 sync detail page + app/admin/sync/pax8/page.tsx + +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`, ``, 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. + + + 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 + + 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. + + + + Task 2: Add the PAX8 card + logo to the sync overview page + app/admin/sync/page.tsx, public/logos/pax8.ico + +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(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. + + + 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 + + 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. + + + + + +- `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. + + + +- 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. + + + +Create `.planning/quick/260712-ash-add-pax8-to-the-admin-sync-overview-page/260712-ash-SUMMARY.md` when done. +