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 { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { RefreshCw, Terminal, Copy, Cpu } from 'lucide-react';
import { HealthGauge } from '@/components/health/health-gauge';
import { HealthStatsBar } from '@/components/health/health-stats-bar';
import { HealthGroupSection } from '@/components/health/health-group-section';
import { useHealth, type HealthGroup } from '@/hooks/use-health';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { PageShell, PageHeader } from '@/components/page-shell';
import {
MonitorLayout,
KpiRow,
KpiCard,
MonitorGrid,
MonitorCard,
} from '@/components/monitor-layout';
function getOverallStatus(summary: { passed: number; warnings: number; errors: number }) {
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() {
const { t } = useTranslation();
const { data, isLoading, refetch, dataUpdatedAt } = useHealth();
// Use dataUpdatedAt directly instead of storing in state
const lastRefresh = dataUpdatedAt;
// Update relative time display by forcing re-render every second
// Force re-render every second so the relative-time label stays fresh.
const [tick, setTick] = useState(0);
useEffect(() => {
const interval = setInterval(() => setTick((t) => t + 1), 1000);
const interval = setInterval(() => setTick((n) => n + 1), 1000);
return () => clearInterval(interval);
}, []);
// Consume tick to prevent unused variable warning
void tick;
const copyDoctorCommand = () => {
@@ -105,94 +80,46 @@ export function HealthPage() {
};
if (isLoading && !data) {
return <LoadingSkeleton />;
return <HealthLoadingSkeleton />;
}
const overallStatus = data ? getOverallStatus(data.summary) : 'ok';
const sortedGroups = data?.groups ? sortGroupsByIssues(data.groups) : [];
return (
<div className="p-6 max-w-6xl mx-auto space-y-6">
{/* Hero Section - Terminal-inspired control center header */}
<div
className={cn(
'relative overflow-hidden rounded-xl border p-6',
'bg-gradient-to-br from-background via-background to-muted/30'
)}
>
{/* Subtle scan lines effect */}
<div
className="absolute inset-0 opacity-[0.02] pointer-events-none"
style={{
backgroundImage: `repeating-linear-gradient(0deg, transparent, transparent 2px, currentColor 2px, currentColor 3px)`,
}}
/>
{/* Grid pattern background */}
<div className="absolute inset-0 opacity-[0.03] pointer-events-none">
<div
className="absolute inset-0"
style={{
backgroundImage: `radial-gradient(circle at 1px 1px, currentColor 1px, transparent 0)`,
backgroundSize: '24px 24px',
}}
/>
</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">
<PageShell>
<PageHeader
title={
<span className="flex items-center gap-3">
{t('health.systemHealth')}
{data?.version && (
<Badge variant="outline" className="font-mono text-xs">
{t('health.build', { version: data.version })}
</Badge>
)}
</span>
}
description={
<span className="flex flex-wrap items-center gap-2 font-mono text-xs">
<Cpu className="size-3.5" />
<span>{t('health.lastScan')}</span>
<span>{dataUpdatedAt ? formatRelativeTime(dataUpdatedAt, t) : '--'}</span>
<span className="text-muted-foreground/60">|</span>
<span>{t('health.autoRefresh')}</span>
<span className="text-emerald-600 dark:text-emerald-400">30s</span>
</span>
}
actions={
<>
<Button
variant="outline"
size="sm"
onClick={copyDoctorCommand}
className="gap-2 font-mono text-xs"
>
<Terminal className="w-3 h-3" />
<Terminal className="size-3.5" />
ccs doctor
<Copy className="w-3 h-3 opacity-50" />
<Copy className="size-3 opacity-50" />
</Button>
<Button
variant="outline"
@@ -201,63 +128,144 @@ export function HealthPage() {
disabled={isLoading}
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>
<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>
</>
}
/>
<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>
</MonitorLayout>
</PageShell>
);
}
{/* Stats Bar */}
{data && (
<div className="rounded-lg border bg-card p-4">
<HealthStatsBar
total={data.summary.total}
passed={data.summary.passed}
warnings={data.summary.warnings}
errors={data.summary.errors}
info={data.summary.info}
/>
</div>
)}
{/* Health Check Groups - Single column layout */}
{sortedGroups.length > 0 && (
<div className="space-y-3">
{sortedGroups.map((group, index) => (
<HealthGroupSection
key={group.id}
group={group}
defaultOpen={
index < 2 ||
group.checks.some((c) => c.status === 'error' || c.status === 'warning')
}
/>
function HealthLoadingSkeleton() {
return (
<PageShell>
<PageHeader title={<Skeleton className="h-6 w-48" />} />
<MonitorLayout
kpis={
<KpiRow>
{[1, 2, 3, 4].map((i) => (
<Skeleton key={i} className="h-20 rounded-xl" />
))}
</KpiRow>
}
>
<MonitorGrid>
<MonitorCard span={12}>
<Skeleton className="h-48 w-full rounded" />
</MonitorCard>
{[1, 2, 3].map((i) => (
<MonitorCard key={i} span={12}>
<Skeleton className="h-16 w-full rounded" />
</MonitorCard>
))}
</div>
)}
{/* 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>
</MonitorGrid>
</MonitorLayout>
</PageShell>
);
}