diff --git a/app/admin/sync/page.tsx b/app/admin/sync/page.tsx index 6cef917..d41545a 100644 --- a/app/admin/sync/page.tsx +++ b/app/admin/sync/page.tsx @@ -26,6 +26,7 @@ const INTEGRATIONS: IntegrationCard[] = [ { id: 'sentinelone', category: 'EDR/AV', product: 'SentinelOne', description: 'Endpoint agents, threat detections, site coverage, AV health', href: '/admin/sync/sentinelone', logo: '/logos/sentinelone.ico', color: 'purple' }, { id: 'mimecast', category: 'Email Security', product: 'Mimecast', description: 'Message tracking logs, threat events, SIEM data, 120-day retention', href: '/admin/sync/mimecast', logo: '/logos/mimecast.ico', color: 'blue' }, { id: 'duo', category: '2FA / MFA', product: 'Duo Security', description: 'Users, phones, auth logs, groups, integrations across all child accounts', href: '/admin/sync/duo', logo: '/logos/duo.ico', color: 'green' }, + { id: 'pax8', category: 'Licensing', product: 'PAX8', description: 'Companies, subscriptions, products, and license billing', href: '/admin/sync/pax8', logo: '/logos/pax8.ico', color: 'blue' }, ]; const COLOR_MAP: Record = { @@ -59,16 +60,18 @@ export default function SyncOverviewPage() { const [mimecastData, setMimecastData] = useState(null); const [duoData, setDuoData] = useState(null); + const [pax8Data, setPax8Data] = useState(null); const fetchAll = async () => { try { - const [intRes, atRes, itgRes, s1Res, mcRes, duoRes] = await Promise.all([ + const [intRes, atRes, itgRes, s1Res, mcRes, duoRes, pax8Res] = await Promise.all([ fetch('/api/integrations/status'), fetch('/api/sync/last-sync'), fetch('/api/itglue/sync'), fetch('/api/sentinelone/sync'), fetch('/api/mimecast/status'), fetch('/api/duo/status'), + fetch('/api/pax8/sync'), ]); if (intRes.ok) setStatus(await intRes.json()); if (atRes.ok) { @@ -79,6 +82,7 @@ export default function SyncOverviewPage() { if (s1Res.ok) setS1SyncData(await s1Res.json()); if (mcRes.ok) setMimecastData(await mcRes.json()); if (duoRes.ok) setDuoData(await duoRes.json()); + if (pax8Res.ok) setPax8Data(await pax8Res.json()); } catch (e) { console.error(e); } finally { @@ -180,6 +184,16 @@ export default function SyncOverviewPage() { bypass: c.bypass ?? 0, }; } + if (id === 'pax8') { + if (!pax8Data) return null; + const h = pax8Data.history?.[0]; + return { + lastSync: h?.completed_at ?? null, + status: h?.status ?? null, + companies: Number(pax8Data.counts?.companies ?? 0), + subscriptions: Number(pax8Data.counts?.subscriptions ?? 0), + }; + } return null; }; @@ -219,6 +233,11 @@ export default function SyncOverviewPage() { if ((summary as any).bypass > 0) return ; return ; } + if (id === 'pax8') { + if (!summary.lastSync) return ; + if (summary.status === 'failed') return ; + return ; + } return ; }; @@ -411,6 +430,22 @@ export default function SyncOverviewPage() { )} )} + {intg.id === 'pax8' && summary && ( + <> +
+ Last sync + {fmtDate(summary.lastSync)} +
+
+ Companies + {(summary as any).companies?.toLocaleString()} +
+
+ Subscriptions + {(summary as any).subscriptions?.toLocaleString()} +
+ + )} {(intg.id === 'auvik' || intg.id === 'addigy') && (
Status diff --git a/public/logos/pax8.ico b/public/logos/pax8.ico new file mode 100644 index 0000000..70f4d8c Binary files /dev/null and b/public/logos/pax8.ico differ