feat(260712-ash): add PAX8 card to sync overview page

- Append PAX8 entry to INTEGRATIONS with logo, description, and detail link
- Fetch /api/pax8/sync in fetchAll, wire pax8 branches in getSummary/getStatusIcon
- Add PAX8 stats block (last sync, companies, subscriptions) to card render
- Add public/logos/pax8.ico as placeholder (copied from itglue.ico — no network
  access available to fetch the real PAX8 favicon; replace with the real logo
  when convenient)
This commit is contained in:
lorentz 2026-07-12 07:52:21 -04:00
parent 3ac111e021
commit 6ed6c66810
2 changed files with 36 additions and 1 deletions

View file

@ -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<string, { bg: string; border: string }> = {
@ -59,16 +60,18 @@ export default function SyncOverviewPage() {
const [mimecastData, setMimecastData] = useState<any>(null);
const [duoData, setDuoData] = useState<any>(null);
const [pax8Data, setPax8Data] = useState<any>(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 <AlertTriangle className="w-4 h-4 text-red-500" />;
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
}
if (id === 'pax8') {
if (!summary.lastSync) return <Clock className="w-4 h-4 text-muted-foreground" />;
if (summary.status === 'failed') return <XCircle className="w-4 h-4 text-red-500" />;
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
}
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
};
@ -411,6 +430,22 @@ export default function SyncOverviewPage() {
)}
</>
)}
{intg.id === 'pax8' && summary && (
<>
<div className="flex justify-between">
<span>Last sync</span>
<span className="font-medium text-foreground">{fmtDate(summary.lastSync)}</span>
</div>
<div className="flex justify-between">
<span>Companies</span>
<span className="font-medium text-foreground">{(summary as any).companies?.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span>Subscriptions</span>
<span className="font-medium text-foreground">{(summary as any).subscriptions?.toLocaleString()}</span>
</div>
</>
)}
{(intg.id === 'auvik' || intg.id === 'addigy') && (
<div className="flex justify-between">
<span>Status</span>