mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-05 10:23:11 +00:00
feat(ui): redesign home page as Interactive Status Board
- Full-width layout (removed max-w constraints) - Restored CcsLogo in hero section with live clock - Added real-time provider status cards with animated indicators - Health progress bar with color-coded status - Icon-based quick launch grid (G/C/A/K/L/D shortcuts) - System navigation panel with all major routes - Shared resources summary with counts - Added Progress UI component for status bars - Subtle grid background for control center aesthetic Design Proposal 4: Interactive Status Board implementation
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const Progress = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||||
|
>(({ className, value, ...props }, ref) => (
|
||||||
|
<ProgressPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn('relative h-2 w-full overflow-hidden rounded-full bg-primary/20', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ProgressPrimitive.Indicator
|
||||||
|
className="h-full w-full flex-1 bg-primary transition-all"
|
||||||
|
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||||
|
/>
|
||||||
|
</ProgressPrimitive.Root>
|
||||||
|
));
|
||||||
|
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Progress };
|
||||||
+454
-294
@@ -1,140 +1,150 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Progress } from '@/components/ui/progress';
|
||||||
import {
|
import {
|
||||||
Key,
|
Key,
|
||||||
Zap,
|
Zap,
|
||||||
Users,
|
Users,
|
||||||
Search,
|
|
||||||
Stethoscope,
|
Stethoscope,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Terminal,
|
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
Command,
|
Activity,
|
||||||
|
Clock,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
AlertCircle,
|
||||||
|
Cpu,
|
||||||
|
Database,
|
||||||
|
Settings,
|
||||||
|
Terminal,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
Circle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import { CcsLogo } from '@/components/ccs-logo';
|
||||||
import { useOverview } from '@/hooks/use-overview';
|
import { useOverview } from '@/hooks/use-overview';
|
||||||
import { useSharedSummary } from '@/hooks/use-shared';
|
import { useSharedSummary } from '@/hooks/use-shared';
|
||||||
|
import { useHealth } from '@/hooks/use-health';
|
||||||
|
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const HEALTH_STATUS = {
|
// Live clock component
|
||||||
ok: { label: 'Operational', class: 'bg-emerald-500/20 text-emerald-500 border-emerald-500/30' },
|
function LiveClock() {
|
||||||
warning: { label: 'Degraded', class: 'bg-amber-500/20 text-amber-500 border-amber-500/30' },
|
const [time, setTime] = useState(new Date());
|
||||||
error: { label: 'Issues', class: 'bg-red-500/20 text-red-500 border-red-500/30' },
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
// Three Pillars configuration - matches README branding
|
useEffect(() => {
|
||||||
const THREE_PILLARS = [
|
const timer = setInterval(() => setTime(new Date()), 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="font-mono text-4xl font-bold tracking-tight tabular-nums">
|
||||||
|
{time.toLocaleTimeString('en-US', { hour12: false })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provider status configuration
|
||||||
|
const PROVIDERS = [
|
||||||
|
{ id: 'gemini', name: 'Gemini', icon: '◆', color: 'bg-blue-500' },
|
||||||
|
{ id: 'codex', name: 'Codex', icon: '⬡', color: 'bg-green-500' },
|
||||||
|
{ id: 'agy', name: 'Antigravity', icon: '◇', color: 'bg-purple-500' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Quick launch shortcuts - icon based
|
||||||
|
const QUICK_LAUNCH = [
|
||||||
{
|
{
|
||||||
id: 'api',
|
key: 'G',
|
||||||
title: 'API Keys',
|
label: 'Gemini',
|
||||||
description: 'Configure GLM, Kimi with your own API keys',
|
command: 'ccs gemini',
|
||||||
icon: Key,
|
color: 'hover:bg-blue-500/20 hover:border-blue-500/50',
|
||||||
path: '/api',
|
|
||||||
gradient: 'from-orange-500/20 to-amber-500/10',
|
|
||||||
iconBg: 'bg-orange-500/20 text-orange-500',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'oauth',
|
key: 'C',
|
||||||
title: 'OAuth Providers',
|
label: 'Codex',
|
||||||
description: 'Gemini, Codex, Antigravity - zero config',
|
command: 'ccs codex',
|
||||||
icon: Zap,
|
color: 'hover:bg-green-500/20 hover:border-green-500/50',
|
||||||
path: '/cliproxy',
|
|
||||||
gradient: 'from-blue-500/20 to-cyan-500/10',
|
|
||||||
iconBg: 'bg-blue-500/20 text-blue-500',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'accounts',
|
key: 'A',
|
||||||
title: 'Multiple Accounts',
|
label: 'Agy',
|
||||||
description: 'Isolated Claude instances for work & personal',
|
command: 'ccs agy',
|
||||||
icon: Users,
|
color: 'hover:bg-purple-500/20 hover:border-purple-500/50',
|
||||||
path: '/accounts',
|
},
|
||||||
gradient: 'from-violet-500/20 to-purple-500/10',
|
{
|
||||||
iconBg: 'bg-violet-500/20 text-violet-500',
|
key: 'K',
|
||||||
|
label: 'Kimi',
|
||||||
|
command: 'ccs kimi',
|
||||||
|
color: 'hover:bg-amber-500/20 hover:border-amber-500/50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'L',
|
||||||
|
label: 'GLM',
|
||||||
|
command: 'ccs glm',
|
||||||
|
color: 'hover:bg-orange-500/20 hover:border-orange-500/50',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'D',
|
||||||
|
label: 'Default',
|
||||||
|
command: 'ccs',
|
||||||
|
color: 'hover:bg-primary/20 hover:border-primary/50',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Quick launch shortcuts
|
// System status items
|
||||||
const QUICK_LAUNCH = [
|
const SYSTEM_ITEMS = [
|
||||||
{ key: 'G', label: 'Gemini', command: 'ccs gemini' },
|
{ id: 'api', label: 'API Keys', icon: Key, path: '/api' },
|
||||||
{ key: 'C', label: 'Codex', command: 'ccs codex' },
|
{ id: 'oauth', label: 'OAuth', icon: Zap, path: '/cliproxy' },
|
||||||
{ key: 'A', label: 'Agy', command: 'ccs agy' },
|
{ id: 'accounts', label: 'Accounts', icon: Users, path: '/accounts' },
|
||||||
|
{ id: 'health', label: 'Doctor', icon: Stethoscope, path: '/health' },
|
||||||
|
{ id: 'settings', label: 'Settings', icon: Settings, path: '/settings' },
|
||||||
|
{ id: 'shared', label: 'Shared', icon: Database, path: '/shared' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data: overview, isLoading: isOverviewLoading } = useOverview();
|
const { data: overview, isLoading: isOverviewLoading } = useOverview();
|
||||||
const { data: shared, isLoading: isSharedLoading } = useSharedSummary();
|
const { data: shared, isLoading: isSharedLoading } = useSharedSummary();
|
||||||
|
const { data: health, isLoading: isHealthLoading } = useHealth();
|
||||||
|
const { data: cliproxyAuth } = useCliproxyAuth();
|
||||||
|
|
||||||
if (isOverviewLoading || isSharedLoading) {
|
if (isOverviewLoading || isSharedLoading || isHealthLoading) {
|
||||||
return <HomePageSkeleton />;
|
return <HomePageSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const healthInfo = overview?.health?.status
|
const healthPercent = health?.summary
|
||||||
? HEALTH_STATUS[overview.health.status as keyof typeof HEALTH_STATUS]
|
? Math.round((health.summary.passed / health.summary.total) * 100)
|
||||||
: HEALTH_STATUS.ok;
|
: 0;
|
||||||
|
const healthStatus = overview?.health?.status ?? 'ok';
|
||||||
|
|
||||||
const pillarCounts = {
|
// Build provider status from auth data
|
||||||
api: overview?.profiles ?? 0,
|
const providerStatus = PROVIDERS.map((provider) => {
|
||||||
oauth: overview?.cliproxy ?? 0,
|
const authInfo = cliproxyAuth?.authStatus?.find((a) => a.provider === provider.id);
|
||||||
accounts: overview?.accounts ?? 0,
|
return {
|
||||||
};
|
...provider,
|
||||||
|
authenticated: authInfo?.authenticated ?? false,
|
||||||
|
accounts: authInfo?.accounts?.length ?? 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen">
|
<div className="min-h-screen">
|
||||||
{/* Gradient Mesh Background */}
|
{/* Subtle grid background */}
|
||||||
<div className="fixed inset-0 -z-10 overflow-hidden">
|
<div className="fixed inset-0 -z-10 opacity-[0.02]">
|
||||||
<div className="absolute -top-1/2 -right-1/2 w-full h-full bg-gradient-to-bl from-primary/5 via-transparent to-transparent rounded-full blur-3xl" />
|
<div
|
||||||
<div className="absolute -bottom-1/2 -left-1/2 w-full h-full bg-gradient-to-tr from-primary/5 via-transparent to-transparent rounded-full blur-3xl" />
|
className="absolute inset-0"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `linear-gradient(to right, currentColor 1px, transparent 1px), linear-gradient(to bottom, currentColor 1px, transparent 1px)`,
|
||||||
|
backgroundSize: '48px 48px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6 max-w-6xl mx-auto space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
<div className="p-6 lg:p-8 space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
||||||
{/* Hero Header */}
|
{/* Configuration Warning - Full Width */}
|
||||||
<header className="relative">
|
|
||||||
<Card className="overflow-hidden border-primary/10 bg-card/80 backdrop-blur-xl">
|
|
||||||
<CardContent className="p-8">
|
|
||||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="p-2.5 rounded-xl bg-gradient-to-br from-primary/20 to-primary/5 border border-primary/20">
|
|
||||||
<Sparkles className="w-6 h-6 text-primary" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold tracking-tight">CCS</h1>
|
|
||||||
<p className="text-sm text-muted-foreground">Multi-Account AI Management</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Badge variant="outline" className="font-mono text-xs px-3 py-1">
|
|
||||||
v{overview?.version ?? '0.0.0'}
|
|
||||||
</Badge>
|
|
||||||
<Badge variant="outline" className={cn('px-3 py-1 border', healthInfo.class)}>
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-current mr-2 animate-pulse" />
|
|
||||||
{healthInfo.label}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Command Palette Search Bar */}
|
|
||||||
<div className="mt-6 group cursor-pointer" onClick={() => navigate('/health')}>
|
|
||||||
<div className="flex items-center gap-3 px-4 py-3 rounded-xl border border-border/50 bg-muted/30 hover:bg-muted/50 hover:border-primary/30 transition-all">
|
|
||||||
<Search className="w-4 h-4 text-muted-foreground" />
|
|
||||||
<span className="text-sm text-muted-foreground flex-1">
|
|
||||||
Search commands, profiles, or run diagnostics...
|
|
||||||
</span>
|
|
||||||
<kbd className="hidden sm:inline-flex h-6 items-center gap-1 rounded border bg-muted px-2 font-mono text-xs text-muted-foreground">
|
|
||||||
<Command className="w-3 h-3" />K
|
|
||||||
</kbd>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Configuration Warning */}
|
|
||||||
{shared?.symlinkStatus && !shared.symlinkStatus.valid && (
|
{shared?.symlinkStatus && !shared.symlinkStatus.valid && (
|
||||||
<Alert
|
<Alert
|
||||||
variant="warning"
|
variant="warning"
|
||||||
@@ -146,237 +156,387 @@ export function HomePage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Three Pillars Section */}
|
{/* Main Status Board Grid - Full Width */}
|
||||||
<section className="space-y-4">
|
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6">
|
||||||
<div className="flex items-center gap-2">
|
{/* Left Column: Hero + System Health */}
|
||||||
<h2 className="text-lg font-semibold tracking-tight">The Three Pillars</h2>
|
<div className="xl:col-span-4 space-y-6">
|
||||||
<span className="text-xs text-muted-foreground">— What do you want to do?</span>
|
{/* Hero Card with Logo */}
|
||||||
</div>
|
<Card className="overflow-hidden border-border/50 bg-card/80 backdrop-blur-sm">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<CardContent className="p-6">
|
||||||
{THREE_PILLARS.map((pillar) => {
|
<div className="flex items-center gap-4 mb-6">
|
||||||
const Icon = pillar.icon;
|
<CcsLogo size="lg" showText={false} />
|
||||||
const count = pillarCounts[pillar.id as keyof typeof pillarCounts];
|
<div>
|
||||||
return (
|
<h1 className="text-2xl font-bold tracking-tight">CCS</h1>
|
||||||
<Card
|
<p className="text-sm text-muted-foreground">Control Center</p>
|
||||||
key={pillar.id}
|
</div>
|
||||||
className={cn(
|
<Badge variant="outline" className="ml-auto font-mono text-xs">
|
||||||
'group cursor-pointer overflow-hidden transition-all duration-300',
|
v{overview?.version ?? '0.0.0'}
|
||||||
'hover:shadow-lg hover:shadow-primary/5 hover:-translate-y-1',
|
</Badge>
|
||||||
'border-border/50 bg-gradient-to-br',
|
</div>
|
||||||
pillar.gradient
|
|
||||||
)}
|
|
||||||
onClick={() => navigate(pillar.path)}
|
|
||||||
>
|
|
||||||
<CardContent className="p-6 space-y-4">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'p-3 rounded-xl transition-transform group-hover:scale-110',
|
|
||||||
pillar.iconBg
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon className="w-6 h-6" />
|
|
||||||
</div>
|
|
||||||
<span className="text-3xl font-bold font-mono opacity-80">{count}</span>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<h3 className="font-semibold text-lg group-hover:text-primary transition-colors">
|
|
||||||
{pillar.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
|
||||||
{pillar.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center text-sm text-primary opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<span>Configure</span>
|
|
||||||
<ArrowRight className="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform" />
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Quick Launch & Health Grid */}
|
{/* Live Clock */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="text-center py-6 border-y border-border/30">
|
||||||
{/* Quick Launch */}
|
<LiveClock />
|
||||||
<Card className="lg:col-span-2 border-border/50 bg-card/80 backdrop-blur-sm">
|
<p className="text-xs text-muted-foreground mt-2 uppercase tracking-widest">
|
||||||
<CardContent className="p-6 space-y-4">
|
System Time
|
||||||
<div className="flex items-center justify-between">
|
</p>
|
||||||
<h3 className="font-semibold flex items-center gap-2">
|
</div>
|
||||||
<Terminal className="w-4 h-4 text-muted-foreground" />
|
|
||||||
Quick Launch
|
{/* Quick Stats */}
|
||||||
</h3>
|
<div className="grid grid-cols-3 gap-4 mt-6">
|
||||||
<span className="text-xs text-muted-foreground">Click to copy command</span>
|
<div className="text-center">
|
||||||
</div>
|
<p className="text-2xl font-bold font-mono text-primary">
|
||||||
<div className="grid grid-cols-3 sm:grid-cols-6 gap-3">
|
{overview?.profiles ?? 0}
|
||||||
{QUICK_LAUNCH.map((item) => (
|
</p>
|
||||||
<button
|
<p className="text-xs text-muted-foreground">Profiles</p>
|
||||||
key={item.key}
|
</div>
|
||||||
className="group flex flex-col items-center gap-2 p-4 rounded-xl border border-border/50 bg-muted/20 hover:bg-primary/10 hover:border-primary/30 transition-all"
|
<div className="text-center">
|
||||||
onClick={() => navigator.clipboard.writeText(item.command)}
|
<p className="text-2xl font-bold font-mono text-primary">
|
||||||
title={item.command}
|
{overview?.cliproxy ?? 0}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">OAuth</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-2xl font-bold font-mono text-primary">
|
||||||
|
{overview?.accounts ?? 0}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">Accounts</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* System Health Card */}
|
||||||
|
<Card
|
||||||
|
className="overflow-hidden border-border/50 bg-card/80 backdrop-blur-sm cursor-pointer hover:border-primary/30 transition-colors"
|
||||||
|
onClick={() => navigate('/health')}
|
||||||
|
>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-semibold flex items-center gap-2">
|
||||||
|
<Activity className="w-4 h-4 text-muted-foreground" />
|
||||||
|
System Health
|
||||||
|
</h3>
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
'px-2 py-0.5 text-xs',
|
||||||
|
healthStatus === 'ok' &&
|
||||||
|
'bg-emerald-500/10 text-emerald-500 border-emerald-500/30',
|
||||||
|
healthStatus === 'warning' &&
|
||||||
|
'bg-amber-500/10 text-amber-500 border-amber-500/30',
|
||||||
|
healthStatus === 'error' && 'bg-red-500/10 text-red-500 border-red-500/30'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<kbd className="w-10 h-10 flex items-center justify-center rounded-lg bg-muted font-mono text-lg font-bold group-hover:bg-primary/20 group-hover:text-primary transition-colors">
|
{healthStatus === 'ok' ? (
|
||||||
{item.key}
|
<CheckCircle2 className="w-3 h-3 mr-1" />
|
||||||
</kbd>
|
) : healthStatus === 'warning' ? (
|
||||||
<span className="text-xs text-muted-foreground group-hover:text-primary transition-colors">
|
<AlertCircle className="w-3 h-3 mr-1" />
|
||||||
{item.label}
|
) : (
|
||||||
</span>
|
<XCircle className="w-3 h-3 mr-1" />
|
||||||
|
)}
|
||||||
|
{healthStatus === 'ok'
|
||||||
|
? 'Healthy'
|
||||||
|
: healthStatus === 'warning'
|
||||||
|
? 'Degraded'
|
||||||
|
: 'Issues'}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground">Health Score</span>
|
||||||
|
<span className="font-mono font-bold">{healthPercent}%</span>
|
||||||
|
</div>
|
||||||
|
<Progress
|
||||||
|
value={healthPercent}
|
||||||
|
className={cn(
|
||||||
|
'h-2',
|
||||||
|
healthStatus === 'ok' && '[&>[role=progressbar]]:bg-emerald-500',
|
||||||
|
healthStatus === 'warning' && '[&>[role=progressbar]]:bg-amber-500',
|
||||||
|
healthStatus === 'error' && '[&>[role=progressbar]]:bg-red-500'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between text-xs text-muted-foreground">
|
||||||
|
<span>{health?.summary?.passed ?? 0} passed</span>
|
||||||
|
<span>{health?.summary?.warnings ?? 0} warnings</span>
|
||||||
|
<span>{health?.summary?.errors ?? 0} errors</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end mt-4 text-xs text-primary">
|
||||||
|
<span>View Details</span>
|
||||||
|
<ArrowRight className="w-3 h-3 ml-1" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center Column: Provider Status + Quick Launch */}
|
||||||
|
<div className="xl:col-span-5 space-y-6">
|
||||||
|
{/* Provider Status Board */}
|
||||||
|
<Card className="overflow-hidden border-border/50 bg-card/80 backdrop-blur-sm">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="font-semibold flex items-center gap-2">
|
||||||
|
<Cpu className="w-4 h-4 text-muted-foreground" />
|
||||||
|
Provider Status
|
||||||
|
</h3>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/cliproxy')}
|
||||||
|
className="text-xs text-primary hover:underline flex items-center gap-1"
|
||||||
|
>
|
||||||
|
Manage <ArrowRight className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
))}
|
</div>
|
||||||
{/* Account shortcuts */}
|
|
||||||
{overview?.accounts && overview.accounts > 0 && (
|
<div className="space-y-4">
|
||||||
<>
|
{providerStatus.map((provider) => (
|
||||||
<button
|
<div
|
||||||
className="group flex flex-col items-center gap-2 p-4 rounded-xl border border-border/50 bg-muted/20 hover:bg-violet-500/10 hover:border-violet-500/30 transition-all"
|
key={provider.id}
|
||||||
onClick={() => navigate('/accounts')}
|
className="flex items-center gap-4 p-3 rounded-lg border border-border/50 bg-muted/20 hover:bg-muted/40 transition-colors"
|
||||||
title="Switch account"
|
|
||||||
>
|
>
|
||||||
<div className="w-10 h-10 flex items-center justify-center rounded-lg bg-muted font-mono text-lg font-bold group-hover:bg-violet-500/20 group-hover:text-violet-500 transition-colors">
|
{/* Status Indicator */}
|
||||||
<Users className="w-5 h-5" />
|
<div className="relative">
|
||||||
|
<Circle
|
||||||
|
className={cn(
|
||||||
|
'w-3 h-3 fill-current',
|
||||||
|
provider.authenticated ? 'text-emerald-500' : 'text-muted-foreground/30'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{provider.authenticated && (
|
||||||
|
<span className="absolute inset-0 animate-ping">
|
||||||
|
<Circle className="w-3 h-3 fill-emerald-500 opacity-50" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-muted-foreground group-hover:text-violet-500 transition-colors">
|
|
||||||
Accounts
|
{/* Provider Info */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-medium">{provider.name}</span>
|
||||||
|
{provider.authenticated && (
|
||||||
|
<Badge variant="outline" className="text-[10px] px-1.5 py-0">
|
||||||
|
{provider.accounts} {provider.accounts === 1 ? 'account' : 'accounts'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{provider.authenticated ? 'Connected' : 'Not authenticated'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Bar */}
|
||||||
|
<div className="w-24">
|
||||||
|
<Progress
|
||||||
|
value={provider.authenticated ? 100 : 0}
|
||||||
|
className={cn(
|
||||||
|
'h-1.5',
|
||||||
|
provider.authenticated && '[&>[role=progressbar]]:bg-emerald-500'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Quick Launch Grid */}
|
||||||
|
<Card className="overflow-hidden border-border/50 bg-card/80 backdrop-blur-sm">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<h3 className="font-semibold flex items-center gap-2">
|
||||||
|
<Terminal className="w-4 h-4 text-muted-foreground" />
|
||||||
|
Quick Launch
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs text-muted-foreground">Click to copy</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-6 gap-3">
|
||||||
|
{QUICK_LAUNCH.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.key}
|
||||||
|
className={cn(
|
||||||
|
'group flex flex-col items-center gap-2 p-4 rounded-xl border border-border/50 bg-muted/10 transition-all',
|
||||||
|
item.color
|
||||||
|
)}
|
||||||
|
onClick={() => navigator.clipboard.writeText(item.command)}
|
||||||
|
title={item.command}
|
||||||
|
>
|
||||||
|
<kbd className="w-10 h-10 flex items-center justify-center rounded-lg bg-background border border-border/50 font-mono text-lg font-bold group-hover:border-current transition-colors">
|
||||||
|
{item.key}
|
||||||
|
</kbd>
|
||||||
|
<span className="text-[10px] text-muted-foreground group-hover:text-current transition-colors">
|
||||||
|
{item.label}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</>
|
))}
|
||||||
)}
|
|
||||||
{/* Doctor shortcut */}
|
|
||||||
<button
|
|
||||||
className="group flex flex-col items-center gap-2 p-4 rounded-xl border border-border/50 bg-muted/20 hover:bg-emerald-500/10 hover:border-emerald-500/30 transition-all"
|
|
||||||
onClick={() => navigate('/health')}
|
|
||||||
title="Run diagnostics"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 flex items-center justify-center rounded-lg bg-muted font-mono text-lg font-bold group-hover:bg-emerald-500/20 group-hover:text-emerald-500 transition-colors">
|
|
||||||
<Stethoscope className="w-5 h-5" />
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-muted-foreground group-hover:text-emerald-500 transition-colors">
|
|
||||||
Doctor
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Health & Resources */}
|
|
||||||
<Card className="border-border/50 bg-card/80 backdrop-blur-sm">
|
|
||||||
<CardContent className="p-6 space-y-6">
|
|
||||||
{/* Health Summary */}
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h3 className="font-semibold text-sm text-muted-foreground uppercase tracking-wider">
|
|
||||||
System Health
|
|
||||||
</h3>
|
|
||||||
<div
|
|
||||||
className="flex items-center justify-between p-4 rounded-xl bg-muted/30 hover:bg-muted/50 cursor-pointer transition-colors"
|
|
||||||
onClick={() => navigate('/health')}
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'w-3 h-3 rounded-full',
|
|
||||||
overview?.health?.status === 'ok'
|
|
||||||
? 'bg-emerald-500'
|
|
||||||
: overview?.health?.status === 'warning'
|
|
||||||
? 'bg-amber-500'
|
|
||||||
: 'bg-red-500'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<span className="font-mono text-2xl font-bold">
|
|
||||||
{overview?.health?.passed ?? 0}/{overview?.health?.total ?? 0}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-muted-foreground">checks passed</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Resources Summary */}
|
{/* Right Column: Navigation + Resources */}
|
||||||
<div className="space-y-3">
|
<div className="xl:col-span-3 space-y-6">
|
||||||
<h3 className="font-semibold text-sm text-muted-foreground uppercase tracking-wider">
|
{/* System Navigation */}
|
||||||
Shared Resources
|
<Card className="overflow-hidden border-border/50 bg-card/80 backdrop-blur-sm">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<h3 className="font-semibold flex items-center gap-2 mb-4">
|
||||||
|
<Sparkles className="w-4 h-4 text-muted-foreground" />
|
||||||
|
Navigation
|
||||||
</h3>
|
</h3>
|
||||||
<div
|
|
||||||
className="grid grid-cols-3 gap-2 cursor-pointer"
|
<div className="space-y-2">
|
||||||
onClick={() => navigate('/shared')}
|
{SYSTEM_ITEMS.map((item) => {
|
||||||
>
|
const Icon = item.icon;
|
||||||
<div className="flex flex-col items-center p-3 rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors">
|
return (
|
||||||
<span className="font-mono text-xl font-bold text-primary">
|
<button
|
||||||
|
key={item.id}
|
||||||
|
className="w-full flex items-center gap-3 p-3 rounded-lg border border-border/50 bg-muted/10 hover:bg-muted/30 hover:border-primary/30 transition-all text-left group"
|
||||||
|
onClick={() => navigate(item.path)}
|
||||||
|
>
|
||||||
|
<Icon className="w-4 h-4 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||||
|
<span className="flex-1 text-sm">{item.label}</span>
|
||||||
|
<ArrowRight className="w-3 h-3 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Shared Resources */}
|
||||||
|
<Card
|
||||||
|
className="overflow-hidden border-border/50 bg-card/80 backdrop-blur-sm cursor-pointer hover:border-primary/30 transition-colors"
|
||||||
|
onClick={() => navigate('/shared')}
|
||||||
|
>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="font-semibold flex items-center gap-2">
|
||||||
|
<Database className="w-4 h-4 text-muted-foreground" />
|
||||||
|
Shared Resources
|
||||||
|
</h3>
|
||||||
|
<ArrowRight className="w-4 h-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div className="text-center p-3 rounded-lg bg-muted/20">
|
||||||
|
<p className="text-xl font-bold font-mono text-primary">
|
||||||
{shared?.commands ?? 0}
|
{shared?.commands ?? 0}
|
||||||
</span>
|
</p>
|
||||||
<span className="text-xs text-muted-foreground">cmds</span>
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wide">
|
||||||
|
Commands
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center p-3 rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors">
|
<div className="text-center p-3 rounded-lg bg-muted/20">
|
||||||
<span className="font-mono text-xl font-bold text-primary">
|
<p className="text-xl font-bold font-mono text-primary">
|
||||||
{shared?.skills ?? 0}
|
{shared?.skills ?? 0}
|
||||||
</span>
|
</p>
|
||||||
<span className="text-xs text-muted-foreground">skills</span>
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wide">
|
||||||
|
Skills
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center p-3 rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors">
|
<div className="text-center p-3 rounded-lg bg-muted/20">
|
||||||
<span className="font-mono text-xl font-bold text-primary">
|
<p className="text-xl font-bold font-mono text-primary">
|
||||||
{shared?.agents ?? 0}
|
{shared?.agents ?? 0}
|
||||||
</span>
|
</p>
|
||||||
<span className="text-xs text-muted-foreground">agents</span>
|
<p className="text-[10px] text-muted-foreground uppercase tracking-wide">
|
||||||
|
Agents
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
|
||||||
|
{/* Uptime Indicator */}
|
||||||
|
<Card className="overflow-hidden border-border/50 bg-card/80 backdrop-blur-sm">
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-emerald-500" />
|
||||||
|
<div className="absolute inset-0 w-3 h-3 rounded-full bg-emerald-500 animate-ping opacity-50" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">System Online</p>
|
||||||
|
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
Config server running
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skeleton loader component
|
// Skeleton loader component - Full Width
|
||||||
function HomePageSkeleton() {
|
function HomePageSkeleton() {
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-6xl mx-auto space-y-8">
|
<div className="p-6 lg:p-8 space-y-6">
|
||||||
{/* Hero Skeleton */}
|
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6">
|
||||||
<div className="rounded-xl border p-8">
|
{/* Left Column */}
|
||||||
<div className="flex items-center gap-4">
|
<div className="xl:col-span-4 space-y-6">
|
||||||
<Skeleton className="h-12 w-12 rounded-xl" />
|
<div className="rounded-xl border p-6 space-y-6">
|
||||||
<div>
|
<div className="flex items-center gap-4">
|
||||||
<Skeleton className="h-7 w-[120px] mb-2" />
|
<Skeleton className="h-12 w-12 rounded" />
|
||||||
<Skeleton className="h-4 w-[200px]" />
|
<div>
|
||||||
</div>
|
<Skeleton className="h-6 w-20 mb-2" />
|
||||||
</div>
|
<Skeleton className="h-4 w-28" />
|
||||||
<Skeleton className="h-12 w-full mt-6 rounded-xl" />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Skeleton className="h-16 w-full" />
|
||||||
{/* Pillars Skeleton */}
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
{[1, 2, 3].map((i) => (
|
||||||
{[1, 2, 3].map((i) => (
|
<Skeleton key={i} className="h-12" />
|
||||||
<div key={i} className="p-6 border rounded-xl space-y-4">
|
))}
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<Skeleton className="h-12 w-12 rounded-xl" />
|
|
||||||
<Skeleton className="h-8 w-8" />
|
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="h-6 w-32" />
|
|
||||||
<Skeleton className="h-4 w-full" />
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="rounded-xl border p-6">
|
||||||
</div>
|
<Skeleton className="h-4 w-32 mb-4" />
|
||||||
|
<Skeleton className="h-2 w-full mb-2" />
|
||||||
{/* Quick Launch Skeleton */}
|
<Skeleton className="h-3 w-full" />
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
||||||
<div className="lg:col-span-2 border rounded-xl p-6">
|
|
||||||
<Skeleton className="h-6 w-32 mb-4" />
|
|
||||||
<div className="grid grid-cols-6 gap-3">
|
|
||||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
|
||||||
<Skeleton key={i} className="h-20 rounded-xl" />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border rounded-xl p-6 space-y-4">
|
|
||||||
<Skeleton className="h-4 w-24" />
|
{/* Center Column */}
|
||||||
<Skeleton className="h-16 rounded-xl" />
|
<div className="xl:col-span-5 space-y-6">
|
||||||
<Skeleton className="h-4 w-24" />
|
<div className="rounded-xl border p-6 space-y-4">
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<Skeleton className="h-4 w-32" />
|
||||||
{[1, 2, 3].map((i) => (
|
{[1, 2, 3].map((i) => (
|
||||||
<Skeleton key={i} className="h-16 rounded-lg" />
|
<Skeleton key={i} className="h-16 w-full rounded-lg" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="rounded-xl border p-6">
|
||||||
|
<Skeleton className="h-4 w-32 mb-4" />
|
||||||
|
<div className="grid grid-cols-6 gap-3">
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
|
<Skeleton key={i} className="h-20 rounded-xl" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column */}
|
||||||
|
<div className="xl:col-span-3 space-y-6">
|
||||||
|
<div className="rounded-xl border p-6 space-y-2">
|
||||||
|
<Skeleton className="h-4 w-24 mb-4" />
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
|
<Skeleton key={i} className="h-10 w-full rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl border p-6">
|
||||||
|
<Skeleton className="h-4 w-32 mb-4" />
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<Skeleton key={i} className="h-16 rounded-lg" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user