docs(260712-ash): pre-dispatch plan for PAX8 admin sync UI
This commit is contained in:
parent
770cc836ef
commit
a8e5afe73a
1 changed files with 159 additions and 0 deletions
|
|
@ -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"
|
||||
---
|
||||
|
||||
<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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue