feat(ui): migrate health page to design system Monitor archetype

Hero gauge + status copy moved into a MonitorCard variant=terminal,
preserving the ccs doctor / live-ops aesthetic per locked decision #4.

HealthStatsBar replaced by a 4-column KpiRow with severity tones
(Checks / Passed / Warnings / Errors). Each HealthGroupSection becomes
its own MonitorCard while keeping the existing accordion behavior
intact -- so check expansion, sort-by-severity, and live monitoring
continue to work unchanged.

PageHeader carries the version badge, last-scan relative time,
auto-refresh hint, ccs doctor copy action, and refresh button.

Pure layout + visual hierarchy migration -- no new metrics, no new
features. LOC: 263 -> ~245.
This commit is contained in:
Tam Nhu Tran
2026-04-25 13:12:41 -04:00
parent 3ae5f20dc8
commit 5e3e554693
+184 -176
View File
@@ -1,15 +1,31 @@
import { Button } from '@/components/ui/button'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Activity,
AlertTriangle,
CheckCircle2,
Copy,
Cpu,
Info,
RefreshCw,
Terminal,
} from 'lucide-react';
import { toast } from 'sonner';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton'; import { Skeleton } from '@/components/ui/skeleton';
import { RefreshCw, Terminal, Copy, Cpu } from 'lucide-react';
import { HealthGauge } from '@/components/health/health-gauge'; import { HealthGauge } from '@/components/health/health-gauge';
import { HealthStatsBar } from '@/components/health/health-stats-bar';
import { HealthGroupSection } from '@/components/health/health-group-section'; import { HealthGroupSection } from '@/components/health/health-group-section';
import { useHealth, type HealthGroup } from '@/hooks/use-health'; import { useHealth, type HealthGroup } from '@/hooks/use-health';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { toast } from 'sonner'; import { PageShell, PageHeader } from '@/components/page-shell';
import { useEffect, useState } from 'react'; import {
import { useTranslation } from 'react-i18next'; MonitorLayout,
KpiRow,
KpiCard,
MonitorGrid,
MonitorCard,
} from '@/components/monitor-layout';
function getOverallStatus(summary: { passed: number; warnings: number; errors: number }) { function getOverallStatus(summary: { passed: number; warnings: number; errors: number }) {
if (summary.errors > 0) return 'error'; if (summary.errors > 0) return 'error';
@@ -41,57 +57,16 @@ function sortGroupsByIssues(groups: HealthGroup[]): HealthGroup[] {
}); });
} }
function TerminalHeader() {
return (
<div className="font-mono text-sm text-muted-foreground flex items-center gap-2">
<span className="text-green-500">$</span>
<span>ccs doctor</span>
</div>
);
}
function LoadingSkeleton() {
return (
<div className="p-6 max-w-6xl mx-auto space-y-6">
{/* Hero skeleton */}
<div className="rounded-xl border bg-gradient-to-br from-background to-muted/20 p-6">
<div className="flex items-center gap-6">
<Skeleton className="w-[120px] h-[120px] rounded-full" />
<div className="flex-1 space-y-3">
<Skeleton className="h-5 w-48" />
<Skeleton className="h-8 w-64" />
<Skeleton className="h-4 w-32" />
</div>
</div>
</div>
{/* Stats skeleton */}
<Skeleton className="h-16 w-full rounded-lg" />
{/* Groups skeleton */}
<div className="space-y-3">
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-20 rounded-lg" />
))}
</div>
</div>
);
}
export function HealthPage() { export function HealthPage() {
const { t } = useTranslation(); const { t } = useTranslation();
const { data, isLoading, refetch, dataUpdatedAt } = useHealth(); const { data, isLoading, refetch, dataUpdatedAt } = useHealth();
// Use dataUpdatedAt directly instead of storing in state // Force re-render every second so the relative-time label stays fresh.
const lastRefresh = dataUpdatedAt;
// Update relative time display by forcing re-render every second
const [tick, setTick] = useState(0); const [tick, setTick] = useState(0);
useEffect(() => { useEffect(() => {
const interval = setInterval(() => setTick((t) => t + 1), 1000); const interval = setInterval(() => setTick((n) => n + 1), 1000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
// Consume tick to prevent unused variable warning
void tick; void tick;
const copyDoctorCommand = () => { const copyDoctorCommand = () => {
@@ -105,94 +80,46 @@ export function HealthPage() {
}; };
if (isLoading && !data) { if (isLoading && !data) {
return <LoadingSkeleton />; return <HealthLoadingSkeleton />;
} }
const overallStatus = data ? getOverallStatus(data.summary) : 'ok'; const overallStatus = data ? getOverallStatus(data.summary) : 'ok';
const sortedGroups = data?.groups ? sortGroupsByIssues(data.groups) : []; const sortedGroups = data?.groups ? sortGroupsByIssues(data.groups) : [];
return ( return (
<div className="p-6 max-w-6xl mx-auto space-y-6"> <PageShell>
{/* Hero Section - Terminal-inspired control center header */} <PageHeader
<div title={
className={cn( <span className="flex items-center gap-3">
'relative overflow-hidden rounded-xl border p-6', {t('health.systemHealth')}
'bg-gradient-to-br from-background via-background to-muted/30' {data?.version && (
)} <Badge variant="outline" className="font-mono text-xs">
> {t('health.build', { version: data.version })}
{/* Subtle scan lines effect */} </Badge>
<div )}
className="absolute inset-0 opacity-[0.02] pointer-events-none" </span>
style={{ }
backgroundImage: `repeating-linear-gradient(0deg, transparent, transparent 2px, currentColor 2px, currentColor 3px)`, description={
}} <span className="flex flex-wrap items-center gap-2 font-mono text-xs">
/> <Cpu className="size-3.5" />
<span>{t('health.lastScan')}</span>
{/* Grid pattern background */} <span>{dataUpdatedAt ? formatRelativeTime(dataUpdatedAt, t) : '--'}</span>
<div className="absolute inset-0 opacity-[0.03] pointer-events-none"> <span className="text-muted-foreground/60">|</span>
<div <span>{t('health.autoRefresh')}</span>
className="absolute inset-0" <span className="text-emerald-600 dark:text-emerald-400">30s</span>
style={{ </span>
backgroundImage: `radial-gradient(circle at 1px 1px, currentColor 1px, transparent 0)`, }
backgroundSize: '24px 24px', actions={
}} <>
/>
</div>
<div className="relative flex flex-col md:flex-row items-start md:items-center gap-6">
{/* Left: Health Gauge - excludes info from percentage */}
{data && (
<div className="shrink-0">
<HealthGauge
passed={data.summary.passed}
total={data.summary.total - data.summary.info}
status={overallStatus}
size="md"
/>
</div>
)}
{/* Center: Title and status */}
<div className="flex-1 space-y-3">
{/* Terminal prompt */}
<TerminalHeader />
{/* Main title */}
<div className="flex items-center gap-3 flex-wrap">
<h1 className="text-2xl font-bold font-mono tracking-tight">
{t('health.systemHealth')}
</h1>
{data?.version && (
<Badge variant="outline" className="font-mono text-xs bg-muted/50">
{t('health.build', { version: data.version })}
</Badge>
)}
</div>
{/* Status message */}
<div className="flex items-center gap-2 text-sm">
<Cpu className="w-4 h-4 text-muted-foreground" />
<span className="text-muted-foreground">{t('health.lastScan')}</span>
<span className="font-mono">
{lastRefresh ? formatRelativeTime(lastRefresh, t) : '--'}
</span>
<span className="text-muted-foreground">|</span>
<span className="text-muted-foreground">{t('health.autoRefresh')}</span>
<span className="font-mono text-green-500">30s</span>
</div>
</div>
{/* Right: Actions */}
<div className="flex items-center gap-2 shrink-0">
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={copyDoctorCommand} onClick={copyDoctorCommand}
className="gap-2 font-mono text-xs" className="gap-2 font-mono text-xs"
> >
<Terminal className="w-3 h-3" /> <Terminal className="size-3.5" />
ccs doctor ccs doctor
<Copy className="w-3 h-3 opacity-50" /> <Copy className="size-3 opacity-50" />
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -201,63 +128,144 @@ export function HealthPage() {
disabled={isLoading} disabled={isLoading}
className="gap-2" className="gap-2"
> >
<RefreshCw className={cn('w-4 h-4', isLoading && 'animate-spin')} /> <RefreshCw className={cn('size-4', isLoading && 'animate-spin')} />
<span className="hidden sm:inline">{t('health.refresh')}</span> <span className="hidden sm:inline">{t('health.refresh')}</span>
<kbd className="hidden md:inline-flex h-5 items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground">
R
</kbd>
</Button> </Button>
</>
}
/>
<MonitorLayout
kpis={
data && (
<KpiRow>
<KpiCard
label={t('health.checks') ?? 'Checks'}
value={data.summary.total}
hint={`${data.summary.passed} ${t('health.ok') ?? 'OK'}`}
tone="default"
icon={<Activity className="size-4" />}
/>
<KpiCard
label="Passed"
value={data.summary.passed}
tone="positive"
icon={<CheckCircle2 className="size-4" />}
/>
<KpiCard
label="Warnings"
value={data.summary.warnings}
tone={data.summary.warnings > 0 ? 'warning' : 'default'}
icon={<AlertTriangle className="size-4" />}
/>
<KpiCard
label="Errors"
value={data.summary.errors}
tone={data.summary.errors > 0 ? 'negative' : 'default'}
icon={<Info className="size-4" />}
/>
</KpiRow>
)
}
>
<MonitorGrid>
{/* Hero: overall health gauge with terminal aesthetic */}
{data && (
<MonitorCard
span={12}
variant="terminal"
title={
<span className="flex items-center gap-2 font-mono">
<span className="text-emerald-500">$</span>
ccs doctor
</span>
}
meta={data.summary.info > 0 ? `${data.summary.info} info` : undefined}
>
<div className="flex flex-col items-center gap-6 py-2 sm:flex-row sm:justify-center">
<HealthGauge
passed={data.summary.passed}
total={data.summary.total - data.summary.info}
status={overallStatus}
size="md"
/>
<div className="text-center font-mono text-sm text-emerald-300 sm:text-left">
<p className="text-xs uppercase tracking-wider opacity-70">Status</p>
<p className="text-2xl font-bold">
{overallStatus === 'ok' && 'All systems nominal'}
{overallStatus === 'warning' && 'Attention required'}
{overallStatus === 'error' && 'Errors detected'}
</p>
<p className="mt-1 text-xs opacity-70">
{data.summary.passed}/{data.summary.total - data.summary.info} non-informational
checks passing
</p>
</div>
</div>
</MonitorCard>
)}
{/* Each health group becomes its own MonitorCard */}
{sortedGroups.map((group, index) => (
<MonitorCard key={group.id} span={12}>
<HealthGroupSection
group={group}
defaultOpen={
index < 2 ||
group.checks.some((c) => c.status === 'error' || c.status === 'warning')
}
/>
</MonitorCard>
))}
</MonitorGrid>
{/* Footer */}
<div className="flex items-center justify-between border-t pt-4 text-xs text-muted-foreground">
<div className="flex items-center gap-4">
<span>
{t('health.version')} <span className="font-mono">{data?.version ?? '--'}</span>
</span>
<span>
{t('health.platform')}{' '}
<span className="font-mono">
{typeof navigator !== 'undefined' ? navigator.platform : 'linux'}
</span>
</span>
</div>
<div className="flex items-center gap-2">
<div className="size-2 animate-pulse rounded-full bg-emerald-500" />
<span>{t('health.liveMonitoring')}</span>
</div> </div>
</div> </div>
</div> </MonitorLayout>
</PageShell>
);
}
{/* Stats Bar */} function HealthLoadingSkeleton() {
{data && ( return (
<div className="rounded-lg border bg-card p-4"> <PageShell>
<HealthStatsBar <PageHeader title={<Skeleton className="h-6 w-48" />} />
total={data.summary.total} <MonitorLayout
passed={data.summary.passed} kpis={
warnings={data.summary.warnings} <KpiRow>
errors={data.summary.errors} {[1, 2, 3, 4].map((i) => (
info={data.summary.info} <Skeleton key={i} className="h-20 rounded-xl" />
/> ))}
</div> </KpiRow>
)} }
>
{/* Health Check Groups - Single column layout */} <MonitorGrid>
{sortedGroups.length > 0 && ( <MonitorCard span={12}>
<div className="space-y-3"> <Skeleton className="h-48 w-full rounded" />
{sortedGroups.map((group, index) => ( </MonitorCard>
<HealthGroupSection {[1, 2, 3].map((i) => (
key={group.id} <MonitorCard key={i} span={12}>
group={group} <Skeleton className="h-16 w-full rounded" />
defaultOpen={ </MonitorCard>
index < 2 ||
group.checks.some((c) => c.status === 'error' || c.status === 'warning')
}
/>
))} ))}
</div> </MonitorGrid>
)} </MonitorLayout>
</PageShell>
{/* Footer metadata */}
<div className="flex items-center justify-between text-xs text-muted-foreground border-t pt-4">
<div className="flex items-center gap-4">
<span>
{t('health.version')} <span className="font-mono">{data?.version ?? '--'}</span>
</span>
<span>
{t('health.platform')}{' '}
<span className="font-mono">
{typeof navigator !== 'undefined' ? navigator.platform : 'linux'}
</span>
</span>
</div>
<div className="flex items-center gap-2">
<div className="w-2 h-2 rounded-full bg-green-500 animate-pulse" />
<span>{t('health.liveMonitoring')}</span>
</div>
</div>
</div>
); );
} }