+
+
+ {check.fix}
+
{check.fixable && check.status !== 'ok' && (
@@ -172,9 +146,9 @@ export function HealthCheckItem({ check }: { check: HealthCheck }) {
size="sm"
onClick={() => fixMutation.mutate(check.id)}
disabled={fixMutation.isPending}
- className="h-auto py-3 px-6 shadow-sm shrink-0"
+ className="h-7 px-3 text-xs"
>
-
+
Apply Fix
)}
diff --git a/ui/src/components/health-gauge.tsx b/ui/src/components/health-gauge.tsx
new file mode 100644
index 00000000..2936b726
--- /dev/null
+++ b/ui/src/components/health-gauge.tsx
@@ -0,0 +1,91 @@
+import { cn } from '@/lib/utils';
+
+interface HealthGaugeProps {
+ passed: number;
+ total: number;
+ status: 'ok' | 'warning' | 'error';
+ size?: 'sm' | 'md' | 'lg';
+}
+
+const sizeConfig = {
+ sm: { dimension: 80, strokeWidth: 6, fontSize: 'text-lg', labelSize: 'text-[10px]' },
+ md: { dimension: 120, strokeWidth: 8, fontSize: 'text-3xl', labelSize: 'text-xs' },
+ lg: { dimension: 160, strokeWidth: 10, fontSize: 'text-4xl', labelSize: 'text-sm' },
+};
+
+const statusColors = {
+ ok: { stroke: '#22C55E', glow: 'rgba(34, 197, 94, 0.4)' },
+ warning: { stroke: '#EAB308', glow: 'rgba(234, 179, 8, 0.4)' },
+ error: { stroke: '#EF4444', glow: 'rgba(239, 68, 68, 0.4)' },
+};
+
+export function HealthGauge({ passed, total, status, size = 'md' }: HealthGaugeProps) {
+ const config = sizeConfig[size];
+ const colors = statusColors[status];
+ const percentage = total > 0 ? Math.round((passed / total) * 100) : 0;
+
+ const radius = (config.dimension - config.strokeWidth) / 2;
+ const circumference = 2 * Math.PI * radius;
+ const strokeDashoffset = circumference - (percentage / 100) * circumference;
+ const center = config.dimension / 2;
+
+ return (
+
+
+ {/* Center content */}
+
+
+ {percentage}
+
+
+ health
+
+
+
+ );
+}
diff --git a/ui/src/components/health-group-section.tsx b/ui/src/components/health-group-section.tsx
new file mode 100644
index 00000000..9b0e4d30
--- /dev/null
+++ b/ui/src/components/health-group-section.tsx
@@ -0,0 +1,111 @@
+import { ChevronDown, Monitor, Settings, Users, Shield, Zap } from 'lucide-react';
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
+import { HealthCheckItem } from '@/components/health-check-item';
+import { type HealthGroup } from '@/hooks/use-health';
+import { cn } from '@/lib/utils';
+import { useState } from 'react';
+
+const groupIcons: Record
= {
+ Monitor,
+ Settings,
+ Users,
+ Shield,
+ Zap,
+};
+
+interface HealthGroupSectionProps {
+ group: HealthGroup;
+ defaultOpen?: boolean;
+}
+
+export function HealthGroupSection({ group, defaultOpen = true }: HealthGroupSectionProps) {
+ const [isOpen, setIsOpen] = useState(defaultOpen);
+ const Icon = groupIcons[group.icon] || Monitor;
+
+ const passed = group.checks.filter((c) => c.status === 'ok').length;
+ const total = group.checks.length;
+ const hasErrors = group.checks.some((c) => c.status === 'error');
+ const hasWarnings = group.checks.some((c) => c.status === 'warning');
+ const percentage = Math.round((passed / total) * 100);
+
+ // Determine status color
+ const statusColor = hasErrors
+ ? 'text-red-500'
+ : hasWarnings
+ ? 'text-yellow-500'
+ : 'text-green-500';
+ const progressColor = hasErrors ? 'bg-red-500' : hasWarnings ? 'bg-yellow-500' : 'bg-green-500';
+
+ return (
+
+
+ {/* Group header */}
+
+
+
+
+ {/* Checks list */}
+
+
+ {group.checks.map((check) => (
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/ui/src/components/health-stats-bar.tsx b/ui/src/components/health-stats-bar.tsx
new file mode 100644
index 00000000..042a3f89
--- /dev/null
+++ b/ui/src/components/health-stats-bar.tsx
@@ -0,0 +1,85 @@
+import { cn } from '@/lib/utils';
+
+interface HealthStatsBarProps {
+ total: number;
+ passed: number;
+ warnings: number;
+ errors: number;
+ info: number;
+}
+
+interface StatItemProps {
+ label: string;
+ value: number;
+ color: string;
+ bgColor: string;
+}
+
+function StatItem({ label, value, color, bgColor }: StatItemProps) {
+ return (
+
+
+
+ {label}
+
+
{value}
+
+ );
+}
+
+export function HealthStatsBar({ total, passed, warnings, errors, info }: HealthStatsBarProps) {
+ // Calculate percentages for the progress bar
+ const passedPct = (passed / total) * 100;
+ const warningPct = (warnings / total) * 100;
+ const errorPct = (errors / total) * 100;
+ const infoPct = (info / total) * 100;
+
+ return (
+
+ {/* Progress bar visualization */}
+
+ {errorPct > 0 && (
+
+ )}
+ {warningPct > 0 && (
+
+ )}
+ {infoPct > 0 && (
+
+ )}
+ {passedPct > 0 && (
+
+ )}
+
+
+ {/* Stats row */}
+
+
+
+ Checks
+
+ {total}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui/src/index.css b/ui/src/index.css
index 62db957d..1cdd21ed 100644
--- a/ui/src/index.css
+++ b/ui/src/index.css
@@ -120,11 +120,12 @@
}
body {
@apply bg-background text-foreground;
- font-family: 'Fira Sans', system-ui, sans-serif;
+ font-family: 'IBM Plex Sans', 'Fira Sans', system-ui, sans-serif;
margin: 0;
}
code,
- pre {
- font-family: 'Fira Code', monospace;
+ pre,
+ .font-mono {
+ font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
}
diff --git a/ui/src/pages/health.tsx b/ui/src/pages/health.tsx
index 2e3bcd31..7c8b5d94 100644
--- a/ui/src/pages/health.tsx
+++ b/ui/src/pages/health.tsx
@@ -1,58 +1,14 @@
import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
-import {
- RefreshCw,
- CheckCircle2,
- AlertTriangle,
- XCircle,
- Info,
- Monitor,
- Settings,
- Users,
- Shield,
- Zap,
- Stethoscope,
- Copy,
- Terminal,
-} from 'lucide-react';
-import { HealthCheckItem } from '@/components/health-check-item';
+import { RefreshCw, Terminal, Copy, Cpu } from 'lucide-react';
+import { HealthGauge } from '@/components/health-gauge';
+import { HealthStatsBar } from '@/components/health-stats-bar';
+import { HealthGroupSection } from '@/components/health-group-section';
import { useHealth, type HealthGroup } from '@/hooks/use-health';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
-
-const groupIcons: Record = {
- Monitor,
- Settings,
- Users,
- Shield,
- Zap,
-};
-
-const statusConfig = {
- ok: {
- icon: CheckCircle2,
- label: 'All Systems Operational',
- color: 'text-green-600',
- bg: 'bg-green-500/10',
- border: 'border-green-500/20',
- },
- warning: {
- icon: AlertTriangle,
- label: 'Some Issues Detected',
- color: 'text-yellow-500',
- bg: 'bg-yellow-500/10',
- border: 'border-yellow-500/20',
- },
- error: {
- icon: XCircle,
- label: 'Action Required',
- color: 'text-red-500',
- bg: 'bg-red-500/10',
- border: 'border-red-500/20',
- },
-};
+import { useEffect, useState } from 'react';
function getOverallStatus(summary: { passed: number; warnings: number; errors: number }) {
if (summary.errors > 0) return 'error';
@@ -60,121 +16,60 @@ function getOverallStatus(summary: { passed: number; warnings: number; errors: n
return 'ok';
}
-function HealthGroupSection({ group }: { group: HealthGroup }) {
- const Icon = groupIcons[group.icon] || Monitor;
-
- const groupPassed = group.checks.filter((c) => c.status === 'ok').length;
- const groupTotal = group.checks.length;
- const hasIssues = group.checks.some((c) => c.status === 'error' || c.status === 'warning');
-
- return (
-
-
-
-
-
-
-
- {group.name}
-
-
- {groupPassed}/{groupTotal}
-
-
-
-
-
- {group.checks.map((check) => (
-
- ))}
-
-
-
- );
+function formatRelativeTime(timestamp: number): string {
+ const seconds = Math.floor((Date.now() - timestamp) / 1000);
+ if (seconds < 5) return 'just now';
+ if (seconds < 60) return `${seconds}s ago`;
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) return `${minutes}m ago`;
+ const hours = Math.floor(minutes / 60);
+ return `${hours}h ago`;
}
-function SummaryCard({
- label,
- value,
- icon: Icon,
- color,
-}: {
- label: string;
- value: number;
- icon: typeof CheckCircle2;
- color: string;
-}) {
+function sortGroupsByIssues(groups: HealthGroup[]): HealthGroup[] {
+ return [...groups].sort((a, b) => {
+ const aErrors = a.checks.filter((c) => c.status === 'error').length;
+ const bErrors = b.checks.filter((c) => c.status === 'error').length;
+ const aWarnings = a.checks.filter((c) => c.status === 'warning').length;
+ const bWarnings = b.checks.filter((c) => c.status === 'warning').length;
+ if (aErrors !== bErrors) return bErrors - aErrors;
+ return bWarnings - aWarnings;
+ });
+}
+
+function TerminalHeader() {
return (
-
-
-
-
-
+
+ $
+ ccs doctor
+
);
}
function LoadingSkeleton() {
return (
- {/* Hero Skeleton */}
-
-
-
-
-
-
+ {/* Hero skeleton */}
+
- {/* Summary Skeleton */}
-
+ {/* Stats skeleton */}
+
+
+ {/* Groups skeleton */}
+
{[1, 2, 3, 4].map((i) => (
))}
-
- {/* Groups Skeleton */}
-
- {[1, 2, 3, 4].map((i) => (
-
-
-
-
-
-
-
- {[1, 2, 3].map((j) => (
-
- ))}
-
-
-
-
- ))}
-
);
}
@@ -182,33 +77,53 @@ function LoadingSkeleton() {
export function HealthPage() {
const { data, isLoading, refetch, dataUpdatedAt } = useHealth();
- const formatTime = (timestamp: number) => {
- return new Date(timestamp).toLocaleTimeString();
- };
+ // Use dataUpdatedAt directly instead of storing in state
+ const lastRefresh = dataUpdatedAt;
+
+ // Update relative time display by forcing re-render every second
+ const [tick, setTick] = useState(0);
+ useEffect(() => {
+ const interval = setInterval(() => setTick((t) => t + 1), 1000);
+ return () => clearInterval(interval);
+ }, []);
+ // Consume tick to prevent unused variable warning
+ void tick;
const copyDoctorCommand = () => {
navigator.clipboard.writeText('ccs doctor');
toast.success('Copied to clipboard');
};
+ const handleRefresh = () => {
+ refetch();
+ toast.info('Refreshing health checks...');
+ };
+
if (isLoading && !data) {
return
;
}
const overallStatus = data ? getOverallStatus(data.summary) : 'ok';
- const status = statusConfig[overallStatus];
- const StatusIcon = status.icon;
+ const sortedGroups = data?.groups ? sortGroupsByIssues(data.groups) : [];
return (
- {/* Hero Section */}
+ {/* Hero Section - Terminal-inspired control center header */}
- {/* Subtle background pattern */}
+ {/* Subtle scan lines effect */}
+
+
+ {/* Grid pattern background */}
-
- {/* Left: Title and Status */}
-
-
-
+
+ {/* Left: Health Gauge - excludes info from percentage */}
+ {data && (
+
+
-
-
-
Health Check
- {data?.version && (
-
- v{data.version}
-
- )}
-
-
-
- {status.label}
-
+ )}
+
+ {/* Center: Title and status */}
+
+ {/* Terminal prompt */}
+
+
+ {/* Main title */}
+
+
System Health
+ {data?.version && (
+
+ build {data.version}
+
+ )}
+
+
+ {/* Status message */}
+
+
+ Last scan:
+
+ {lastRefresh ? formatRelativeTime(lastRefresh) : '--'}
+
+ |
+ Auto-refresh:
+ 30s
{/* Right: Actions */}
-
+
-
-
- {/* Last check time */}
- {dataUpdatedAt && (
-
- Last check: {formatTime(dataUpdatedAt)}
-
- )}
- {/* Summary Stats */}
+ {/* Stats Bar */}
{data && (
-
-
+
-
-
-
)}
- {/* Health Check Groups */}
- {data?.groups && (
-
- {data.groups.map((group) => (
-
-
-
+ {/* Health Check Groups - Single column layout */}
+ {sortedGroups.length > 0 && (
+
+ {sortedGroups.map((group, index) => (
+ c.status === 'error' || c.status === 'warning')
+ }
+ />
))}
)}
- {/* Issues Summary */}
- {data && (data.summary.errors > 0 || data.summary.warnings > 0) && (
-
-
-
-
- Issues Detected
-
-
-
-
- {data.checks
- .filter((c) => c.status === 'error' || c.status === 'warning')
- .map((check) => (
-
- {check.status === 'error' ? (
-
- ) : (
-
- )}
-
-
{check.name}
-
{check.message}
- {check.fix && (
-
- {check.fix}
-
- )}
-
-
- ))}
-
-
-
- )}
+ {/* Footer metadata */}
+
+
+
+ Version {data?.version ?? '--'}
+
+
+ Platform{' '}
+
+ {typeof navigator !== 'undefined' ? navigator.platform : 'linux'}
+
+
+
+
+
+
Live monitoring active
+
+
);
}