wulf-pulse/app/status/page.tsx
lorentz ab78e7bd4f refactor(design): adopt TanStack DataTable on /veeam-analysis, fill gaps
Follow-on polish for the nav-design overhaul (#9bfb575).

- /veeam-analysis migrates the bespoke TicketRow + custom pagination to
  the new DataTable using getRowCanExpand + renderSubRow. Drops ~85
  lines of fragment/colspan markup in favor of the standard pattern.

- PageHeader on the last common stragglers — /settings,
  /settings/security, /sentinelone/coverage, /sentinelone/mappings.
  Settings is reachable from the new top-bar UserMenu so it had to
  match the rest of the visual system.

- /dashboard and /status load with the new Skeleton helpers
  (SkeletonRows, SkeletonChart, SkeletonTable) so loading shells now
  approximate the post-load layout instead of a single h-NN bar.

- DESIGN.md: closed the straggler PageHeader item; deprioritized the
  hard-coded palette audit with a note that ~770 references are mostly
  semantic via the documented bg-{hue}-500/15 / text-{hue}-700 recipe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 09:42:04 -04:00

540 lines
20 KiB
TypeScript

/* /status — System health dashboard.
*
* Pulls from:
* GET /api/dashboard/integration-health (live API check + token expiry)
* GET /api/dashboard/overview (syncHealth array)
*
* Surfaces what's wrong so the dashboard can stay focused on operational
* KPIs. Polls every 60 s while the page is visible. */
'use client';
import { useEffect, useState } from 'react';
import { PageHeader } from '@/components/navigation/page-header';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { SkeletonTable } from '@/components/ui/skeleton-helpers';
import { EmptyState } from '@/components/ui/empty-state';
import { StatusLight, type StatusLightState } from '@/components/ui/status-light';
import { StatusBadge } from '@/components/ui/status-badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { WorkerPulse } from '@/components/status/worker-pulse';
import {
AlertTriangle,
KeyRound,
RefreshCw,
ShieldCheck,
Plug,
Clock,
Activity,
} from 'lucide-react';
// ── Types ────────────────────────────────────────────────────────────
type IntegrationCategory =
| 'psa' | 'rmm' | 'docs' | 'security' | 'backup'
| 'network' | 'identity' | 'mdm' | 'mail'
| 'finance' | 'productivity' | 'llm';
interface IntegrationHealthItem {
key: string;
name: string;
category: IntegrationCategory;
status: 'ok' | 'auth_failed' | 'unreachable' | 'not_configured' | 'unknown' | 'disabled';
configured: boolean;
latencyMs?: number;
error?: string | null;
tokenExpiry?: {
envVar: string;
expiresAt: string;
daysRemaining: number;
subject?: string | null;
} | null;
checkedAt: string;
}
interface IntegrationHealthResponse {
items: IntegrationHealthItem[];
summary: {
total: number;
ok: number;
failed: number;
notConfigured: number;
disabled: number;
expiringWithin14Days: number;
expired: number;
hasIssues: boolean;
};
}
interface SyncHealthItem {
id: string;
name: string;
syncType: string;
isEnabled: boolean;
lastRun: string | null;
lastStatus: string | null;
lastError: string | null;
nextRun: string | null;
}
interface OverviewResponse {
syncHealth: SyncHealthItem[];
}
interface WorkerSnapshot {
name: string;
lastActivity: string | null;
inFlight: number;
oneHour: { success: number; failure: number };
}
interface WorkersResponse {
workers: WorkerSnapshot[];
}
const WORKER_FRESHNESS: Record<string, number> = {
Analyzer: 5,
'RMM Overshell': 10,
'Sync scheduler': 60,
};
// ── Helpers ──────────────────────────────────────────────────────────
const STALE_HOURS = 24;
const POLL_MS = 60_000;
const CATEGORY_LABELS: Record<IntegrationCategory, string> = {
psa: 'PSA',
rmm: 'RMM',
docs: 'Documentation',
security: 'Security',
backup: 'Backup',
network: 'Network',
identity: 'Identity',
mdm: 'MDM',
mail: 'Mail',
finance: 'Finance',
productivity: 'Productivity',
llm: 'LLM',
};
const CATEGORY_ORDER: IntegrationCategory[] = [
'psa', 'rmm', 'docs', 'security', 'backup',
'network', 'identity', 'mdm', 'mail',
'finance', 'productivity', 'llm',
];
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`;
}
function isStale(iso: string | null): boolean {
if (!iso) return true;
return Date.now() - new Date(iso).getTime() > STALE_HOURS * 3600_000;
}
function integrationLight(item: IntegrationHealthItem): StatusLightState {
if (item.status === 'disabled') return 'idle';
const tokenExpired = item.tokenExpiry && item.tokenExpiry.daysRemaining <= 0;
const tokenExpiring =
item.tokenExpiry &&
item.tokenExpiry.daysRemaining > 0 &&
item.tokenExpiry.daysRemaining <= 14;
if (item.status === 'auth_failed' || item.status === 'unreachable' || tokenExpired) {
return 'error';
}
if (tokenExpiring) return 'warn';
if (item.status === 'ok') return 'ok';
if (item.status === 'not_configured') return 'idle';
return 'idle';
}
function syncLight(item: SyncHealthItem): StatusLightState {
if (!item.isEnabled) return 'idle';
if (item.lastStatus === 'failed') return 'error';
if (isStale(item.lastRun)) return 'warn';
if (item.lastStatus === 'success') return 'ok';
return 'idle';
}
// ── Page ─────────────────────────────────────────────────────────────
export default function StatusPage() {
const [health, setHealth] = useState<IntegrationHealthResponse | null>(null);
const [overview, setOverview] = useState<OverviewResponse | null>(null);
const [workers, setWorkers] = useState<WorkerSnapshot[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [refreshing, setRefreshing] = useState(false);
async function load(force = false) {
setRefreshing(true);
try {
const [hRes, oRes, wRes] = await Promise.all([
fetch(`/api/dashboard/integration-health${force ? '?refresh=1' : ''}`, { cache: 'no-store' }),
fetch('/api/dashboard/overview', { cache: 'no-store' }),
fetch('/api/status/workers', { cache: 'no-store' }),
]);
if (hRes.ok) setHealth((await hRes.json()) as IntegrationHealthResponse);
if (oRes.ok) setOverview((await oRes.json()) as OverviewResponse);
if (wRes.ok) {
const j = (await wRes.json()) as WorkersResponse;
setWorkers(j.workers);
}
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load');
} finally {
setRefreshing(false);
}
}
useEffect(() => {
void load();
const id = setInterval(() => void load(), POLL_MS);
return () => clearInterval(id);
}, []);
// Roll-up
const overall: StatusLightState = !health
? 'idle'
: health.summary.failed > 0 || health.summary.expired > 0
? 'error'
: health.summary.expiringWithin14Days > 0 ||
(overview?.syncHealth.some((s) => syncLight(s) === 'error') ?? false)
? 'warn'
: 'ok';
const overallTitle = !health
? 'Loading…'
: overall === 'error'
? `${health.summary.failed} integration${health.summary.failed === 1 ? '' : 's'} failing`
: overall === 'warn'
? health.summary.expiringWithin14Days > 0
? `${health.summary.expiringWithin14Days} token${health.summary.expiringWithin14Days === 1 ? '' : 's'} expiring soon`
: 'Some sync tasks degraded'
: 'All systems operational';
// Group integrations
const grouped = (() => {
if (!health) return null;
const map: Record<string, IntegrationHealthItem[]> = {};
for (const item of health.items) {
(map[item.category] ??= []).push(item);
}
return map;
})();
const expiring = health?.items
.filter((i) => i.tokenExpiry && i.tokenExpiry.daysRemaining <= 30)
.sort((a, b) => (a.tokenExpiry!.daysRemaining ?? 999) - (b.tokenExpiry!.daysRemaining ?? 999));
const failingSyncs = overview?.syncHealth.filter((s) => syncLight(s) === 'error');
const failingIntegrations = health?.items.filter(
(i) => i.status === 'auth_failed' || i.status === 'unreachable',
);
return (
<>
<PageHeader
title="System Status"
description={overallTitle}
breadcrumbs={[{ label: 'Status' }]}
accent
watermark
actions={
<>
<StatusLight state={overall} size="lg" label={overallTitle} />
<Button
onClick={() => void load(true)}
variant="outline"
size="sm"
disabled={refreshing}
>
<RefreshCw className={`h-4 w-4 mr-2 ${refreshing ? 'animate-spin' : ''}`} />
Refresh
</Button>
</>
}
/>
<div className="container mx-auto px-6 py-6 space-y-6">
{error && (
<Alert variant="destructive">
<AlertTitle>Failed to load status</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* CONDITIONAL BANNER -------------------------------------------- */}
{(failingIntegrations?.length || failingSyncs?.length) ? (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>Action needed</AlertTitle>
<AlertDescription>
<ul className="list-disc pl-5 mt-1 space-y-0.5">
{failingIntegrations?.map((i) => (
<li key={i.key}>
<span className="font-medium">{i.name}</span> {' '}
{i.status === 'auth_failed' ? 'authentication failed' : 'unreachable'}
{i.error && <span className="text-muted-foreground"> · {i.error.slice(0, 120)}</span>}
</li>
))}
{failingSyncs?.map((s) => (
<li key={s.id}>
<span className="font-medium">{s.name}</span> sync failed
{s.lastError && <span className="text-muted-foreground"> · {s.lastError.slice(0, 120)}</span>}
</li>
))}
</ul>
</AlertDescription>
</Alert>
) : null}
{/* INTEGRATION TILES --------------------------------------------- */}
<section className="space-y-4">
<div className="flex items-center gap-2">
<Plug className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Integrations
</h2>
{health && (
<span className="text-xs text-muted-foreground">
{health.summary.ok} of {health.summary.total - health.summary.notConfigured} healthy
</span>
)}
</div>
{!grouped ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{[1, 2, 3, 4, 5, 6].map((i) => <Skeleton key={i} className="h-24" />)}
</div>
) : (
<div className="space-y-6">
{CATEGORY_ORDER.filter((c) => grouped[c]?.length).map((category) => (
<div key={category} className="space-y-2">
<h3 className="metric-label">{CATEGORY_LABELS[category]}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{grouped[category]
.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map((item) => <IntegrationTile key={item.key} item={item} />)}
</div>
</div>
))}
</div>
)}
</section>
{/* WORKERS ------------------------------------------------------- */}
<section className="space-y-4">
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-muted-foreground" />
<h2 className="text-sm font-semibold uppercase tracking-wide text-muted-foreground">
Workers
</h2>
</div>
{!workers ? (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => <Skeleton key={i} className="h-36" />)}
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{workers.map((w) => (
<WorkerPulse
key={w.name}
worker={w}
freshnessMinutes={WORKER_FRESHNESS[w.name]}
/>
))}
</div>
)}
</section>
{/* TOKEN EXPIRY -------------------------------------------------- */}
{expiring && expiring.length > 0 && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<KeyRound className="h-4 w-4" />
Tokens expiring within 30 days
</CardTitle>
</CardHeader>
<CardContent>
<div className="divide-y divide-border">
{expiring.map((item) => {
const days = item.tokenExpiry!.daysRemaining;
const tone = days <= 0 ? 'error' : days <= 14 ? 'warn' : 'pending';
return (
<div key={item.key} className="flex items-center justify-between py-2 text-sm">
<div className="min-w-0">
<span className="font-medium">{item.name}</span>
<span className="text-muted-foreground"> · {item.tokenExpiry!.envVar}</span>
</div>
<StatusBadge tone={tone}>
{days <= 0 ? `expired ${Math.abs(days)} d ago` : `${days} d`}
</StatusBadge>
</div>
);
})}
</div>
</CardContent>
</Card>
)}
{/* SYNC HEALTH --------------------------------------------------- */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Clock className="h-4 w-4" />
Scheduled syncs
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{!overview ? (
<SkeletonTable rows={6} cols={5} />
) : overview.syncHealth.length === 0 ? (
<div className="px-6 py-6">
<EmptyState
icon={Clock}
title="No scheduled syncs"
description="Configure schedules in Admin to populate this list."
action={{ label: 'Open admin', href: '/admin' }}
/>
</div>
) : (
<Table>
<TableHeader className="bg-muted/40">
<TableRow>
<TableHead>Schedule</TableHead>
<TableHead>Type</TableHead>
<TableHead>Last run</TableHead>
<TableHead>Next run</TableHead>
<TableHead className="text-right">Status</TableHead>
<TableHead className="w-10" aria-label="indicator" />
</TableRow>
</TableHeader>
<TableBody>
{overview.syncHealth.map((s) => (
<TableRow key={s.id}>
<TableCell className="font-medium">{s.name}</TableCell>
<TableCell className="num text-muted-foreground">{s.syncType}</TableCell>
<TableCell className="num text-muted-foreground">{relTime(s.lastRun)}</TableCell>
<TableCell className="num text-muted-foreground">{relTime(s.nextRun)}</TableCell>
<TableCell className="text-right">
{s.isEnabled
? s.lastStatus === 'failed'
? <StatusBadge tone="error">failed</StatusBadge>
: isStale(s.lastRun)
? <StatusBadge tone="warn">stale</StatusBadge>
: s.lastStatus === 'success'
? <StatusBadge tone="ok">success</StatusBadge>
: <StatusBadge tone="neutral">idle</StatusBadge>
: <StatusBadge tone="inactive">off</StatusBadge>}
</TableCell>
<TableCell className="text-right">
<StatusLight state={syncLight(s)} size="sm" label={s.lastStatus ?? 'idle'} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
{/* COMPLIANCE FOOTER -------------------------------------------- */}
{health && (
<p className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground">
<ShieldCheck className="h-3.5 w-3.5" />
<span><span className="num">{health.summary.ok}</span> healthy</span>
<span>·</span>
<span><span className="num">{health.summary.failed}</span> failing</span>
<span>·</span>
<span><span className="num">{health.summary.notConfigured}</span> unconfigured</span>
{health.summary.disabled > 0 && (
<>
<span>·</span>
<span><span className="num">{health.summary.disabled}</span> disabled</span>
</>
)}
<span>·</span>
<span><span className="num">{health.summary.expiringWithin14Days}</span> expiring</span>
<span>·</span>
<span>last checked {relTime(health.items[0]?.checkedAt ?? null)}</span>
</p>
)}
</div>
</>
);
}
// ── Integration tile ────────────────────────────────────────────────
function IntegrationTile({ item }: { item: IntegrationHealthItem }) {
const light = integrationLight(item);
const expired = item.tokenExpiry && item.tokenExpiry.daysRemaining <= 0;
const expiringSoon =
item.tokenExpiry && item.tokenExpiry.daysRemaining > 0 && item.tokenExpiry.daysRemaining <= 14;
let detail: string | null = null;
if (item.status === 'disabled') detail = 'disabled by operator';
else if (item.status === 'auth_failed') detail = 'authentication failed';
else if (item.status === 'unreachable') detail = 'unreachable';
else if (expired) detail = `token expired ${Math.abs(item.tokenExpiry!.daysRemaining)} d ago`;
else if (expiringSoon) detail = `token expires in ${item.tokenExpiry!.daysRemaining} d`;
else if (item.status === 'not_configured') detail = 'not configured';
else if (item.status === 'ok' && item.latencyMs !== undefined) detail = `${item.latencyMs} ms`;
else if (item.status === 'unknown' && item.configured) detail = 'configured';
return (
<div
className={
'rounded-md border bg-card px-3 py-3 flex items-start gap-3 ' +
(light === 'error'
? 'border-destructive/40'
: item.status === 'disabled'
? 'border-border opacity-60'
: 'border-border')
}
>
<StatusLight state={light} size="md" label={item.status} className="mt-1" />
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<p className="font-medium truncate">{item.name}</p>
{(expired || expiringSoon) && (
<KeyRound className={`h-3.5 w-3.5 shrink-0 ${expired ? 'text-destructive' : 'text-amber-500'}`} />
)}
</div>
{detail && (
<p className={`text-xs num truncate ${light === 'error' ? 'text-destructive' : 'text-muted-foreground'}`}>
{detail}
</p>
)}
{item.error && light === 'error' && (
<p className="text-xs text-muted-foreground/80 truncate" title={item.error}>
{item.error.slice(0, 80)}
</p>
)}
</div>
</div>
);
}