2025-11-19 14:18:16 -05:00
'use client' ;
import { useState , useEffect } from 'react' ;
import Link from 'next/link' ;
2026-02-20 10:28:15 -05:00
import { Button } from '@/components/ui/button' ;
import { RefreshCw , CheckCircle2 , XCircle , AlertTriangle , Clock , Loader2 , ChevronRight } from 'lucide-react' ;
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
import { PageHeader } from '@/components/navigation/page-header' ;
2026-02-20 10:28:15 -05:00
interface IntegrationCard {
id : string ;
category : string ;
product : string ;
description : string ;
href : string ;
logo : string ;
color : string ;
}
const INTEGRATIONS : IntegrationCard [ ] = [
2026-02-27 05:31:31 -05:00
{ id : 'autotask' , category : 'PSA' , product : 'Autotask' , description : 'Tickets, companies, contacts, time entries, configuration items' , href : '/admin/sync/autotask' , logo : '/logos/autotask.ico' , color : 'red' } ,
{ id : 'itglue' , category : 'Documentation' , product : 'IT Glue' , description : 'Organizations, configurations, contacts, passwords, flexible assets, documents, domains' , href : '/admin/sync/itglue' , logo : '/logos/itglue.ico' , color : 'blue' } ,
{ id : 'veeam' , category : 'Backup' , product : 'Veeam VSPC' , description : 'Agent jobs, backup jobs, protected workloads, compliance' , href : '/admin/sync/veeam' , logo : '/logos/veeam.ico' , color : 'green' } ,
{ id : 'datto-rmm' , category : 'RMM' , product : 'Datto RMM' , description : 'Device monitoring, alerts, patch management, remote access' , href : '/admin/sync/datto-rmm' , logo : '/logos/datto-rmm.ico' , color : 'orange' } ,
{ id : 'auvik' , category : 'NMS' , product : 'Auvik' , description : 'Network devices, topology, tenant mappings' , href : '/admin/sync/auvik' , logo : '/logos/auvik.ico' , color : 'purple' } ,
{ id : 'addigy' , category : 'Apple RMM' , product : 'Addigy' , description : 'macOS/iOS device management, policies, compliance' , href : '/admin/sync/addigy' , logo : '/logos/addigy.ico' , color : 'gray' } ,
{ 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' } ,
2026-03-17 16:23:47 -04:00
{ 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' } ,
feat: Add Duo Security card to /admin/sync overview + detail page
- Added Duo card to sync overview grid (category: 2FA/MFA, green)
- Shows accounts, users, phones, auth logs counts + bypass/disabled warning
- Created /admin/sync/duo detail page with:
- Stat cards (accounts, users, phones, auth logs, groups, integrations)
- Parent account summary
- Child accounts table with user counts, matched company, sync time
- Sync Now button with polling for completion
- Created GET /api/duo/status endpoint (counts + last sync + bypass count)
- Added duo.ico logo
2026-03-27 11:14:49 -04:00
{ 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' } ,
2026-07-12 07:52:21 -04:00
{ id : 'pax8' , category : 'Licensing' , product : 'PAX8' , description : 'Companies, subscriptions, products, and license billing' , href : '/admin/sync/pax8' , logo : '/logos/pax8.ico' , color : 'blue' } ,
2026-08-05 20:31:26 -04:00
{ id : 'route53' , category : 'DNS' , product : 'AWS Route 53' , description : 'Hosted zones, DNS records, change history, NS-delegation health' , href : '/admin/sync/route53' , logo : '/logos/route53.svg' , color : 'orange' } ,
2026-02-20 10:28:15 -05:00
] ;
const COLOR_MAP : Record < string , { bg : string ; border : string } > = {
red : { bg : 'bg-red-500/5' , border : 'border-red-500/20' } ,
green : { bg : 'bg-green-500/5' , border : 'border-green-500/20' } ,
blue : { bg : 'bg-blue-500/5' , border : 'border-blue-500/20' } ,
2026-02-27 05:31:31 -05:00
orange : { bg : 'bg-orange-500/5' , border : 'border-orange-500/20' } ,
2026-02-20 10:28:15 -05:00
purple : { bg : 'bg-purple-500/5' , border : 'border-purple-500/20' } ,
gray : { bg : 'bg-muted/20' , border : 'border-border' } ,
} ;
2025-11-19 14:18:16 -05:00
2026-02-20 10:28:15 -05:00
function fmtDate ( d : string | null ) {
if ( ! d ) return 'Never' ;
const date = new Date ( d ) ;
const diff = Date . now ( ) - date . getTime ( ) ;
const mins = Math . floor ( diff / 60000 ) ;
if ( mins < 1 ) return 'Just now' ;
if ( mins < 60 ) return ` ${ mins } m ago ` ;
const hrs = Math . floor ( mins / 60 ) ;
if ( hrs < 24 ) return ` ${ hrs } h ago ` ;
return ` ${ Math . floor ( hrs / 24 ) } d ago ` ;
}
export default function SyncOverviewPage() {
const [ status , setStatus ] = useState < any > ( null ) ;
const [ autotaskSync , setAutotaskSync ] = useState < any > ( null ) ;
const [ loading , setLoading ] = useState ( true ) ;
2026-02-27 05:31:31 -05:00
const [ itglueSyncData , setItglueSyncData ] = useState < any > ( null ) ;
const [ s1SyncData , setS1SyncData ] = useState < any > ( null ) ;
2026-03-17 16:23:47 -04:00
const [ mimecastData , setMimecastData ] = useState < any > ( null ) ;
feat: Add Duo Security card to /admin/sync overview + detail page
- Added Duo card to sync overview grid (category: 2FA/MFA, green)
- Shows accounts, users, phones, auth logs counts + bypass/disabled warning
- Created /admin/sync/duo detail page with:
- Stat cards (accounts, users, phones, auth logs, groups, integrations)
- Parent account summary
- Child accounts table with user counts, matched company, sync time
- Sync Now button with polling for completion
- Created GET /api/duo/status endpoint (counts + last sync + bypass count)
- Added duo.ico logo
2026-03-27 11:14:49 -04:00
const [ duoData , setDuoData ] = useState < any > ( null ) ;
2026-07-12 07:52:21 -04:00
const [ pax8Data , setPax8Data ] = useState < any > ( null ) ;
2026-03-17 16:23:47 -04:00
2026-02-20 10:28:15 -05:00
const fetchAll = async ( ) = > {
try {
2026-07-12 07:52:21 -04:00
const [ intRes , atRes , itgRes , s1Res , mcRes , duoRes , pax8Res ] = await Promise . all ( [
2026-02-20 10:28:15 -05:00
fetch ( '/api/integrations/status' ) ,
fetch ( '/api/sync/last-sync' ) ,
2026-02-27 05:31:31 -05:00
fetch ( '/api/itglue/sync' ) ,
fetch ( '/api/sentinelone/sync' ) ,
2026-03-17 16:23:47 -04:00
fetch ( '/api/mimecast/status' ) ,
feat: Add Duo Security card to /admin/sync overview + detail page
- Added Duo card to sync overview grid (category: 2FA/MFA, green)
- Shows accounts, users, phones, auth logs counts + bypass/disabled warning
- Created /admin/sync/duo detail page with:
- Stat cards (accounts, users, phones, auth logs, groups, integrations)
- Parent account summary
- Child accounts table with user counts, matched company, sync time
- Sync Now button with polling for completion
- Created GET /api/duo/status endpoint (counts + last sync + bypass count)
- Added duo.ico logo
2026-03-27 11:14:49 -04:00
fetch ( '/api/duo/status' ) ,
2026-07-12 07:52:21 -04:00
fetch ( '/api/pax8/sync' ) ,
2026-02-20 10:28:15 -05:00
] ) ;
if ( intRes . ok ) setStatus ( await intRes . json ( ) ) ;
if ( atRes . ok ) {
const d = await atRes . json ( ) ;
setAutotaskSync ( d . lastSync || { } ) ;
}
2026-02-27 05:31:31 -05:00
if ( itgRes . ok ) setItglueSyncData ( await itgRes . json ( ) ) ;
if ( s1Res . ok ) setS1SyncData ( await s1Res . json ( ) ) ;
2026-03-17 16:23:47 -04:00
if ( mcRes . ok ) setMimecastData ( await mcRes . json ( ) ) ;
feat: Add Duo Security card to /admin/sync overview + detail page
- Added Duo card to sync overview grid (category: 2FA/MFA, green)
- Shows accounts, users, phones, auth logs counts + bypass/disabled warning
- Created /admin/sync/duo detail page with:
- Stat cards (accounts, users, phones, auth logs, groups, integrations)
- Parent account summary
- Child accounts table with user counts, matched company, sync time
- Sync Now button with polling for completion
- Created GET /api/duo/status endpoint (counts + last sync + bypass count)
- Added duo.ico logo
2026-03-27 11:14:49 -04:00
if ( duoRes . ok ) setDuoData ( await duoRes . json ( ) ) ;
2026-07-12 07:52:21 -04:00
if ( pax8Res . ok ) setPax8Data ( await pax8Res . json ( ) ) ;
2026-02-20 10:28:15 -05:00
} catch ( e ) {
console . error ( e ) ;
} finally {
setLoading ( false ) ;
2025-11-19 14:18:16 -05:00
}
2026-02-20 10:28:15 -05:00
} ;
useEffect ( ( ) = > { fetchAll ( ) ; } , [ ] ) ;
2025-11-19 14:18:16 -05:00
2026-02-20 10:28:15 -05:00
const getAutotaskSummary = ( ) = > {
if ( ! autotaskSync ) return null ;
const entries = Object . values ( autotaskSync ) as any [ ] ;
if ( ! entries . length ) return null ;
const latest = entries . reduce ( ( a : any , b : any ) = >
new Date ( a . completed_at ) > new Date ( b . completed_at ) ? a : b
) ;
const failed = entries . filter ( ( e : any ) = > e . status === 'failed' ) . length ;
return { lastSync : latest.completed_at , failed , total : entries.length } ;
2025-11-19 14:18:16 -05:00
} ;
2026-02-20 10:28:15 -05:00
const atSummary = getAutotaskSummary ( ) ;
const getSummary = ( id : string ) = > {
if ( ! status ) return null ;
if ( id === 'autotask' ) return atSummary ;
if ( id === 'veeam' ) {
const v = status . veeam ;
if ( ! v ) return null ;
const aj = v . agentJobs ? ? { } ;
const bj = v . backupJobs ? ? { } ;
return {
lastSync : v.lastSync ,
failed : ( aj . failed ? ? 0 ) + ( bj . failed ? ? 0 ) ,
warning : ( aj . warning ? ? 0 ) + ( bj . warning ? ? 0 ) ,
running : aj.running ? ? 0 ,
total : ( aj . total ? ? 0 ) + ( bj . total ? ? 0 ) ,
} ;
}
if ( id === 'datto-rmm' ) {
const d = status . dattoRmm ;
if ( ! d ) return null ;
return {
lastSync : d.lastSync ,
sites : d.sites ? ? 0 ,
devices : d.devices?.total ? ? 0 ,
online : d.devices?.online ? ? 0 ,
offline : d.devices?.offline ? ? 0 ,
openAlerts : d.openAlerts?.total ? ? 0 ,
critical : d.openAlerts?.critical ? ? 0 ,
} ;
}
2026-02-27 05:31:31 -05:00
if ( id === 'itglue' ) {
if ( ! itglueSyncData ) return null ;
const h = itglueSyncData . history ? . [ 0 ] ;
const c = itglueSyncData . counts ? ? { } ;
return {
lastSync : h?.completed_at ? ? null ,
organizations : Number ( c . organizations ? ? 0 ) ,
configurations : Number ( c . configurations ? ? 0 ) ,
totalUpserted : h?.total_upserted ? ? 0 ,
status : h?.status ? ? null ,
} ;
}
if ( id === 'sentinelone' ) {
if ( ! s1SyncData ) return null ;
const h = s1SyncData . history ? . [ 0 ] ;
const c = s1SyncData . counts ? ? { } ;
return {
lastSync : h?.completed_at ? ? null ,
status : h?.status ? ? null ,
sites : Number ( c . sites ? ? 0 ) ,
agents : Number ( c . agents ? ? 0 ) ,
infected : Number ( c . infected ? ? 0 ) ,
threats : Number ( c . threats ? ? 0 ) ,
} ;
}
2026-02-20 10:28:15 -05:00
if ( id === 'auvik' ) return { lastSync : status.auvik?.lastSync , configured : status.auvik?.configured } ;
if ( id === 'addigy' ) return { lastSync : status.addigy?.lastSync , configured : status.addigy?.configured } ;
2026-03-17 16:23:47 -04:00
if ( id === 'mimecast' ) {
if ( ! mimecastData ) return null ;
const s = mimecastData . stats ? ? { } ;
return {
lastSync : s.lastSync ? ? null ,
connected : mimecastData.connected ? ? false ,
messages : s.messages ? ? 0 ,
threats : s.threats ? ? 0 ,
} ;
}
feat: Add Duo Security card to /admin/sync overview + detail page
- Added Duo card to sync overview grid (category: 2FA/MFA, green)
- Shows accounts, users, phones, auth logs counts + bypass/disabled warning
- Created /admin/sync/duo detail page with:
- Stat cards (accounts, users, phones, auth logs, groups, integrations)
- Parent account summary
- Child accounts table with user counts, matched company, sync time
- Sync Now button with polling for completion
- Created GET /api/duo/status endpoint (counts + last sync + bypass count)
- Added duo.ico logo
2026-03-27 11:14:49 -04:00
if ( id === 'duo' ) {
if ( ! duoData ) return null ;
const c = duoData . counts ? ? { } ;
return {
lastSync : duoData.lastSync ? ? null ,
connected : duoData.connected ? ? false ,
accounts : c.accounts ? ? 0 ,
users : c.users ? ? 0 ,
phones : c.phones ? ? 0 ,
authLogs : c.authLogs ? ? 0 ,
2026-03-27 11:41:52 -04:00
bypass : c.bypass ? ? 0 ,
feat: Add Duo Security card to /admin/sync overview + detail page
- Added Duo card to sync overview grid (category: 2FA/MFA, green)
- Shows accounts, users, phones, auth logs counts + bypass/disabled warning
- Created /admin/sync/duo detail page with:
- Stat cards (accounts, users, phones, auth logs, groups, integrations)
- Parent account summary
- Child accounts table with user counts, matched company, sync time
- Sync Now button with polling for completion
- Created GET /api/duo/status endpoint (counts + last sync + bypass count)
- Added duo.ico logo
2026-03-27 11:14:49 -04:00
} ;
}
2026-07-12 07:52:21 -04:00
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 ) ,
} ;
}
2026-02-20 10:28:15 -05:00
return null ;
} ;
const getStatusIcon = ( id : string , summary : any ) = > {
if ( ! summary ) return < Clock className = "w-4 h-4 text-muted-foreground" / > ;
if ( id === 'veeam' ) {
if ( summary . failed > 0 ) return < XCircle className = "w-4 h-4 text-red-500" / > ;
if ( summary . warning > 0 || summary . running > 0 ) return < AlertTriangle className = "w-4 h-4 text-yellow-500" / > ;
return < CheckCircle2 className = "w-4 h-4 text-green-500" / > ;
}
if ( id === 'autotask' ) {
if ( summary . failed > 0 ) return < XCircle className = "w-4 h-4 text-red-500" / > ;
return < CheckCircle2 className = "w-4 h-4 text-green-500" / > ;
}
if ( id === 'datto-rmm' ) {
if ( summary . critical > 0 ) return < XCircle className = "w-4 h-4 text-red-500" / > ;
if ( summary . openAlerts > 0 ) return < AlertTriangle className = "w-4 h-4 text-yellow-500" / > ;
return < CheckCircle2 className = "w-4 h-4 text-green-500" / > ;
}
2026-02-27 05:31:31 -05:00
if ( id === 'itglue' ) {
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" / > ;
}
if ( id === 'sentinelone' ) {
if ( ! summary . lastSync ) return < Clock className = "w-4 h-4 text-muted-foreground" / > ;
if ( summary . infected > 0 ) return < AlertTriangle className = "w-4 h-4 text-red-500" / > ;
return < CheckCircle2 className = "w-4 h-4 text-green-500" / > ;
}
2026-03-17 16:23:47 -04:00
if ( id === 'mimecast' ) {
if ( ! summary . connected ) return < Clock className = "w-4 h-4 text-muted-foreground" / > ;
if ( ( summary as any ) . threats > 0 ) return < AlertTriangle className = "w-4 h-4 text-yellow-500" / > ;
return < CheckCircle2 className = "w-4 h-4 text-green-500" / > ;
}
feat: Add Duo Security card to /admin/sync overview + detail page
- Added Duo card to sync overview grid (category: 2FA/MFA, green)
- Shows accounts, users, phones, auth logs counts + bypass/disabled warning
- Created /admin/sync/duo detail page with:
- Stat cards (accounts, users, phones, auth logs, groups, integrations)
- Parent account summary
- Child accounts table with user counts, matched company, sync time
- Sync Now button with polling for completion
- Created GET /api/duo/status endpoint (counts + last sync + bypass count)
- Added duo.ico logo
2026-03-27 11:14:49 -04:00
if ( id === 'duo' ) {
if ( ! summary . connected ) return < Clock className = "w-4 h-4 text-muted-foreground" / > ;
2026-03-27 11:41:52 -04:00
if ( ( summary as any ) . bypass > 0 ) return < AlertTriangle className = "w-4 h-4 text-red-500" / > ;
feat: Add Duo Security card to /admin/sync overview + detail page
- Added Duo card to sync overview grid (category: 2FA/MFA, green)
- Shows accounts, users, phones, auth logs counts + bypass/disabled warning
- Created /admin/sync/duo detail page with:
- Stat cards (accounts, users, phones, auth logs, groups, integrations)
- Parent account summary
- Child accounts table with user counts, matched company, sync time
- Sync Now button with polling for completion
- Created GET /api/duo/status endpoint (counts + last sync + bypass count)
- Added duo.ico logo
2026-03-27 11:14:49 -04:00
return < CheckCircle2 className = "w-4 h-4 text-green-500" / > ;
}
2026-07-12 07:52:21 -04:00
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" / > ;
}
2026-02-20 10:28:15 -05:00
return < CheckCircle2 className = "w-4 h-4 text-green-500" / > ;
2025-11-19 14:18:16 -05:00
} ;
return (
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< >
< PageHeader
title = "Integrations & Sync"
description = "Manage data sync across all connected platforms"
breadcrumbs = { [ { label : 'Admin' , href : '/admin' } , { label : 'Integrations & Sync' } ] }
accent
actions = {
< Button variant = "outline" size = "sm" onClick = { fetchAll } className = "gap-2" >
< RefreshCw className = "w-4 h-4" / >
Refresh
< / Button >
}
/ >
< div className = "container mx-auto py-4 md:py-8 px-4 space-y-6" >
2026-02-20 10:28:15 -05:00
{ loading ? (
< div className = "flex items-center justify-center py-20" >
< Loader2 className = "w-6 h-6 animate-spin text-muted-foreground" / >
< / div >
) : (
< div className = "grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 items-stretch" >
{ INTEGRATIONS . map ( ( intg ) = > {
const colors = COLOR_MAP [ intg . color ] ;
const summary = getSummary ( intg . id ) ;
return (
< Link key = { intg . id } href = { intg . href } className = "flex" >
< div className = { ` flex flex-col w-full rounded-xl border ${ colors . border } ${ colors . bg } p-5 hover:shadow-md transition-all cursor-pointer group ` } >
{ /* Header: logo + names + chevron */ }
< div className = "flex items-start justify-between mb-4" >
< div className = "flex items-center gap-3" >
< img src = { intg . logo } alt = { intg . product } className = "w-10 h-10 shrink-0 rounded-lg object-contain" / >
< div >
< p className = "text-xs font-semibold uppercase tracking-wider text-muted-foreground leading-none mb-1" >
{ intg . category }
< / p >
< div className = "flex items-center gap-2" >
< span className = "font-bold text-base leading-tight" > { intg . product } < / span >
{ summary && getStatusIcon ( intg . id , summary ) }
< / div >
< / div >
< / div >
< ChevronRight className = "w-4 h-4 text-muted-foreground group-hover:text-foreground transition-colors shrink-0 mt-1" / >
< / div >
{ /* Description */ }
< p className = "text-xs text-muted-foreground leading-relaxed mb-4" > { intg . description } < / p >
{ /* Stats — pushed to bottom */ }
< div className = "mt-auto pt-3 border-t border-border/50 space-y-1.5 text-xs text-muted-foreground" >
{ intg . id === 'autotask' && 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 > Entities tracked < / span >
< span className = "font-medium text-foreground" > { summary . total } < / span >
< / div >
< / >
) }
{ intg . id === 'veeam' && 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 > Total jobs < / span >
< span className = "font-medium text-foreground" > { summary . total } < / span >
< / div >
{ ( summary as any ) . failed > 0 && (
< div className = "flex justify-between text-red-600" >
< span > Failed < / span > < span className = "font-medium" > { ( summary as any ) . failed } < / span >
< / div >
) }
{ ( summary as any ) . running > 0 && (
< div className = "flex justify-between text-blue-600" >
< span > Running ( stalled ? ) < / span > < span className = "font-medium" > { ( summary as any ) . running } < / span >
< / div >
) }
{ ( summary as any ) . warning > 0 && (
< div className = "flex justify-between text-yellow-700" >
< span > Warning < / span > < span className = "font-medium" > { ( summary as any ) . warning } < / span >
< / div >
) }
< / >
) }
{ intg . id === 'datto-rmm' && 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 > Sites / Devices < / span >
< span className = "font-medium text-foreground" > { ( summary as any ) . sites } / { ( summary as any ) . devices } < / span >
< / div >
< div className = "flex justify-between" >
< span > Online / Offline < / span >
< span className = "font-medium text-foreground" > { ( summary as any ) . online } / { ( summary as any ) . offline } < / span >
< / div >
{ ( summary as any ) . openAlerts > 0 && (
< div className = { ` flex justify-between ${ ( summary as any ) . critical > 0 ? 'text-red-600' : 'text-yellow-700' } ` } >
< span > Open alerts < / span >
< span className = "font-medium" > { ( summary as any ) . openAlerts } < / span >
< / div >
) }
< / >
) }
2026-02-27 05:31:31 -05:00
{ intg . id === 'itglue' && 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 > Organizations < / span >
< span className = "font-medium text-foreground" > { ( summary as any ) . organizations ? . toLocaleString ( ) } < / span >
< / div >
< div className = "flex justify-between" >
< span > Configurations < / span >
< span className = "font-medium text-foreground" > { ( summary as any ) . configurations ? . toLocaleString ( ) } < / span >
< / div >
< / >
) }
{ intg . id === 'sentinelone' && 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 > Sites / Agents < / span >
< span className = "font-medium text-foreground" > { ( summary as any ) . sites } / { ( summary as any ) . agents ? . toLocaleString ( ) } < / span >
< / div >
{ ( summary as any ) . infected > 0 && (
< div className = "flex justify-between text-red-600" >
< span > Infected < / span >
< span className = "font-medium" > { ( summary as any ) . infected } < / span >
< / div >
) }
< / >
) }
2026-03-17 16:23:47 -04:00
{ intg . id === 'mimecast' && summary && (
< >
< div className = "flex justify-between" >
< span > Last sync < / span >
< span className = "font-medium text-foreground" > { fmtDate ( ( summary as any ) . lastSync ) } < / span >
< / div >
< div className = "flex justify-between" >
< span > Messages < / span >
< span className = "font-medium text-foreground" > { ( ( summary as any ) . messages ? ? 0 ) . toLocaleString ( ) } < / span >
< / div >
{ ( summary as any ) . threats > 0 && (
< div className = "flex justify-between text-yellow-700" >
< span > Threat events < / span >
< span className = "font-medium" > { ( summary as any ) . threats } < / span >
< / div >
) }
< / >
) }
feat: Add Duo Security card to /admin/sync overview + detail page
- Added Duo card to sync overview grid (category: 2FA/MFA, green)
- Shows accounts, users, phones, auth logs counts + bypass/disabled warning
- Created /admin/sync/duo detail page with:
- Stat cards (accounts, users, phones, auth logs, groups, integrations)
- Parent account summary
- Child accounts table with user counts, matched company, sync time
- Sync Now button with polling for completion
- Created GET /api/duo/status endpoint (counts + last sync + bypass count)
- Added duo.ico logo
2026-03-27 11:14:49 -04:00
{ intg . id === 'duo' && summary && (
< >
< div className = "flex justify-between" >
< span > Last sync < / span >
< span className = "font-medium text-foreground" > { fmtDate ( ( summary as any ) . lastSync ) } < / span >
< / div >
< div className = "flex justify-between" >
< span > Accounts / Users < / span >
< span className = "font-medium text-foreground" > { ( summary as any ) . accounts } / { ( summary as any ) . users ? . toLocaleString ( ) } < / span >
< / div >
< div className = "flex justify-between" >
< span > Phones < / span >
< span className = "font-medium text-foreground" > { ( summary as any ) . phones ? . toLocaleString ( ) } < / span >
< / div >
< div className = "flex justify-between" >
< span > Auth logs < / span >
< span className = "font-medium text-foreground" > { ( summary as any ) . authLogs ? . toLocaleString ( ) } < / span >
< / div >
2026-03-27 11:41:52 -04:00
{ ( summary as any ) . bypass > 0 && (
< div className = "flex justify-between text-red-600" >
< span > Bypass ( MFA skipped ) < / span >
< span className = "font-medium" > { ( summary as any ) . bypass } < / span >
feat: Add Duo Security card to /admin/sync overview + detail page
- Added Duo card to sync overview grid (category: 2FA/MFA, green)
- Shows accounts, users, phones, auth logs counts + bypass/disabled warning
- Created /admin/sync/duo detail page with:
- Stat cards (accounts, users, phones, auth logs, groups, integrations)
- Parent account summary
- Child accounts table with user counts, matched company, sync time
- Sync Now button with polling for completion
- Created GET /api/duo/status endpoint (counts + last sync + bypass count)
- Added duo.ico logo
2026-03-27 11:14:49 -04:00
< / div >
) }
< / >
) }
2026-07-12 07:52:21 -04:00
{ 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 >
< / >
) }
2026-02-20 10:28:15 -05:00
{ ( intg . id === 'auvik' || intg . id === 'addigy' ) && (
< div className = "flex justify-between" >
< span > Status < / span >
< span className = { ` font-medium ${ ( summary as any ) ? . configured ? 'text-green-600' : 'text-muted-foreground' } ` } >
{ ( summary as any ) ? . configured ? 'Connected' : 'Not configured' }
< / span >
< / div >
) }
< / div >
< / div >
< / Link >
) ;
} ) }
< / div >
) }
feat(design): nav/visual overhaul — brand layer, /status route, KPI dashboard, TanStack DataTable
Major UI refresh on the nav-design-improvements branch. Drops 2013-era
inline styles and consolidates patterns behind shared primitives.
Foundation
- New Wulf brand layer in app/styles/brand.css repointing --primary to
the standards-guide blue (#0075AD) with utility classes for numerics
(.num / .num-lg / .num-xl), metric labels, surface tints, and the
wolf-mark watermark
- Switch primary face to IBM Plex Sans + IBM Plex Mono via next/font;
Helvetica/Arial stays in the fallback chain for brand fidelity
- Wordmark subtitle changed from "PSA Management System" to
"Operations console" everywhere it appeared
- Tagline footer ("Don't be afraid to cry") on every non-mobile page
Status moved out of /dashboard
- New /status route with integration tiles grouped by category, sync
health table, worker pulse cards (analyzer / RMM / sync scheduler),
token-expiry section, conditional alert banner
- Top-bar StatusIndicator polls integration health every 60s and links
to /status
- INTEGRATIONS_DISABLED env var suppresses operator-disabled
integrations (e.g. SentinelOne) — no failure noise from broken-on-
purpose entries. Aliases supported (sentinelone → s1, etc.)
Dashboard rebuilt around KPIs
- /api/dashboard/overview adds today snapshot (opened, resolved, open
total, SLA breaches) with delta math
- /api/dashboard/trends backs queue × priority heatmap, 30-day volume
area chart, 30-day mean resolution time line chart, today's active
engineers leaderboard
Components
- StatusBadge driven by lib/status-registry.ts (priority, ticket
status, classification, source, company type, publish, active /
yes-no / billable / approved registries)
- StatusLight (8px geometric square, five states, three sizes)
- EmptyState (shared dashed panel with icon + headline + optional CTA)
- KpiCard with delta indicator and tonal left border
- WulfMark (mark / wordmark variants from /public/branding)
- Skeleton helpers (SkeletonRow / Rows / Card / Chart / Header / Table)
Navigation
- Admin flat link → dropdown with seven shortcuts
- New UserMenu (initials avatar, role badge, settings + sign-out)
- Active-route highlight is now a 2px Wulf-blue underline echoing the
PageHeader rule (consistent across flat links and submenu triggers);
active children inside dropdowns use bg-primary/10
- Submenu width is content-driven (min-w 320 / max-w 440, single col)
- Mobile hamburger via Sheet, reuses the same nav config
Pages migrated
- 16 admin sub-pages adopt PageHeader (with accent prop)
- /addigy-devices: shadcn Table + Checkbox; PageHeader; status badges
- 10 raw <table> blocks across admin/sync/* migrated to shadcn Table
- /veeam-analysis migrated to shadcn Table (kept its expansion logic)
- Detail routes (analyzer ticket, analyzer analysis) get breadcrumbs
DataTable
- Rewritten on @tanstack/react-table v8 in manual mode; external API
unchanged so all 10+ data-browser pages keep working
- New optional props for drill-down rows: getRowCanExpand + renderSubRow
Mobile
- Multi-select Popover gets max-w-[calc(100vw-1rem)] and
collisionPadding so dropdowns can't overflow narrow viewports
- CI filter bar wraps and shrinks; stat pill flows below
Docs
- New ARCHITECTURE.md (load-bearing reference for runtime, data flow,
workers, analyzer pipeline, auth, deployment, gotchas)
- New DESIGN.md (tokens, layout, navigation IA, component vocabulary,
rolling backlog of remaining cleanup)
- CLAUDE.md refreshed with pointers to the two new docs and the
INTEGRATIONS_DISABLED operator config note
- shadcn registry registered as project-level MCP server (.mcp.json)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:33:13 -04:00
< / div >
< / >
2025-11-19 14:18:16 -05:00
) ;
}