/* /dashboard — Operations home. * * KPI-first. Health and sync status moved to /status (linked from the * top-bar StatusLight). This page surfaces: * • Today snapshot — opened, resolved, open total, SLA breaches * • Needs attention — admin housekeeping that pulls a human's eyes * • Recent observations + recent audits * * Trends (volume by day, queue heatmap) will land here next once the * supporting endpoints exist; for now the page is intentionally minimal * and load-fast. */ 'use client'; import { useEffect, useState } from 'react'; import { PageHeader } from '@/components/navigation/page-header'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { EmptyState } from '@/components/ui/empty-state'; import { KpiCard } from '@/components/dashboard/kpi-card'; import { VolumeTrend } from '@/components/dashboard/volume-trend'; import { ResolutionTrend } from '@/components/dashboard/resolution-trend'; import { QueueHeatmap } from '@/components/dashboard/queue-heatmap'; import { ActiveEngineers } from '@/components/dashboard/active-engineers'; import { RefreshCw, Activity, Sparkles, Users, Layers, TrendingUp, Timer, } from 'lucide-react'; interface Overview { today: { openedToday: number; resolvedToday: number; openTotal: number; slaBreaches: number; yesterdayOpened: number; last7DayAvgResolved: number; }; attention: { linkConflicts: number; itglueUnlinked: number; s1Unmapped: number; schedules: { enabled: number; total: number }; }; observations: Array<{ id: string; kind: string; source: string; collectedAt: string; hostname: string | null; companyName: string | null; runId: string | null; }>; audits: Array<{ id: string; generatedAt: string; hostname: string | null; companyName: string | null; overallScore: number | null; fieldGapsCount: number; status: string; }>; stats: { activeCompanies: number; configurationItems: number; xref: { total: number; linked: number }; }; } interface Trends { volumeByDay: Array<{ date: string; count: number }>; resolutionByDay: Array<{ date: string; avgHours: number | null }>; queueHeatmap: Array<{ queueId: number; queueLabel: string; total: number; byPriority: Record; }>; activeEngineers: Array<{ resourceId: string; name: string; hours: number; ticketsTouched: number; }>; } function relTime(iso: string | null): string { if (!iso) return 'never'; const ms = Date.now() - new Date(iso).getTime(); if (ms < 0) return 'in the future'; const min = Math.floor(ms / 60000); if (min < 1) return 'just now'; if (min < 60) return `${min} min ago`; const hr = Math.floor(min / 60); if (hr < 48) return `${hr} h ago`; const day = Math.floor(hr / 24); return `${day} d ago`; } export default function DashboardPage() { const [data, setData] = useState(null); const [trends, setTrends] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); async function load() { setLoading(true); try { const [overviewRes, trendsRes] = await Promise.all([ fetch('/api/dashboard/overview', { cache: 'no-store' }), fetch('/api/dashboard/trends', { cache: 'no-store' }), ]); if (!overviewRes.ok) { const body = (await overviewRes.json().catch(() => ({}))) as { error?: string }; throw new Error(body.error ?? `HTTP ${overviewRes.status}`); } setData((await overviewRes.json()) as Overview); if (trendsRes.ok) { setTrends((await trendsRes.json()) as Trends); } setError(null); } catch (err) { setError(err instanceof Error ? err.message : 'Unknown error'); } finally { setLoading(false); } } useEffect(() => { void load(); }, []); const today = data?.today; const openedDelta = today ? today.openedToday - today.yesterdayOpened : 0; const resolvedDelta = today ? Math.round((today.resolvedToday - today.last7DayAvgResolved) * 10) / 10 : 0; return ( <> Refresh } />
{error && ( Failed to load {error} )} {/* TODAY SNAPSHOT ----------------------------------------------- */}

Today

0 ? 'attention' : 'default' } caption={ today && today.slaBreaches === 0 ? 'All on track' : 'Past due, still open' } loading={!data} />
{/* NEEDS ATTENTION ---------------------------------------------- */}

Needs attention

0 ? 'warn' : 'default' } href="/admin/device-link-conflicts" loading={!data} />
{/* QUEUE POSTURE ------------------------------------------------ */}
Queue posture {!trends ? ( ) : ( )} Active engineers {!trends ? ( ) : ( )}
{/* TRENDS ------------------------------------------------------- */}
Volume · last 30 days {!trends ? ( ) : ( )} Mean resolution time · last 30 days {!trends ? ( ) : ( )}
{/* RECENT ACTIVITY --------------------------------------------- */}
Recent device observations {!data ? ( ) : data.observations.length === 0 ? ( ) : (
{data.observations.map((o) => (
{o.hostname ?? '(unanchored)'}
{o.kind} {o.companyName && · {o.companyName}}
{relTime(o.collectedAt)}
))}
)}
Recent audits {!data ? ( ) : data.audits.length === 0 ? ( ) : (
{data.audits.map((a) => (
{a.hostname ?? '(unanchored)'}
score {a.overallScore?.toFixed(2) ?? '—'} {' · '} {a.fieldGapsCount} gaps {a.companyName && · {a.companyName}}
{relTime(a.generatedAt)}
))}
)}
{/* STATS FOOTER ------------------------------------------------- */} {data && (

{data.stats.activeCompanies} active companies ·{' '} {data.stats.configurationItems.toLocaleString()} configuration items ·{' '} {data.stats.xref.total.toLocaleString()} xref rows{' '} ({data.stats.xref.total > 0 ? Math.round((data.stats.xref.linked / data.stats.xref.total) * 100) : 0}% linked)

)}
); } function RowSkeletons() { return (
); }