import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { createProject, getHostMonitoring, listRecentDeployments, listProjects, type ProjectEntity, type ProjectStats, } from '@/lib/api-client'; import { Search, FolderOpen, X, } from 'lucide-react'; import { useAuthSession } from '@/lib/use-auth-session'; import { useDemoMode } from '@/lib/demo-mode'; import { demoProjects } from '@/lib/demo-data'; const demoDeploys = [ { id: 'd1', name: 'API Gateway', project: 'Core Platform', status: 'DEPLOYED', when: '4m' }, { id: 'd2', name: 'Web Frontend', project: 'Core Platform', status: 'BUILDING', when: '6m' }, { id: 'd3', name: 'Batch Evaluator', project: 'Inference Lab', status: 'FAILED', when: '1h' }, { id: 'd4', name: 'Marketing Site', project: 'Growth Surface', status: 'DEPLOYED', when: '2h' }, ]; function getHealthStatus(stats: ProjectStats): 'healthy' | 'idle' | 'degraded' | 'critical' { if (stats.service_count === 0) return 'idle'; if (stats.running_services === stats.service_count) return 'healthy'; if (stats.running_services > 0) return 'degraded'; return 'critical'; } function healthBadge(health: ReturnType) { switch (health) { case 'healthy': return { label: 'RUNNING', cls: 'v-st-ok' }; case 'idle': return { label: 'IDLE', cls: '' }; case 'degraded': return { label: 'DEGRADED', cls: 'v-st-warn' }; case 'critical': return { label: 'DOWN', cls: 'v-st-fail' }; } } function formatRelative(date?: string): string { if (!date) return '—'; const diff = Date.now() - new Date(date).getTime(); if (diff < 60_000) return 'just now'; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`; if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`; return `${Math.floor(diff / 86_400_000)}d ago`; } function MiniCanvas({ project }: { project: ProjectEntity }) { const count = project.stats.service_count; const running = project.stats.running_services; const shown = Math.min(count, 4); const extra = count - shown; const positions = [ { left: 14, top: 22 }, { left: 96, top: 22 }, { left: 14, top: 62 }, { left: 96, top: 62 }, ]; return (
{count >= 2 ?
: null} {count >= 4 ?
: null} {Array.from({ length: shown }).map((_, i) => (
))} {extra > 0 ? (
+{extra}
) : null}
); } function ProjectCard({ project, href }: { project: ProjectEntity; href: string }) { const navigate = useNavigate(); const health = getHealthStatus(project.stats); const badge = healthBadge(health); const envOk = health === 'healthy'; const envLabel = envOk ? 'production' : health === 'idle' ? 'idle' : health === 'critical' ? 'down' : 'degraded'; return (
navigate(href)} >

{project.name}

{badge.label}
{envLabel} · {project.stats.running_services}/{project.stats.service_count} online {formatRelative(project.updatedAt)}
); } export function ProjectsPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const isDemoMode = useDemoMode(); const [search, setSearch] = useState(''); const [isCreateOpen, setCreateOpen] = useState(false); const [form, setForm] = useState({ name: '', description: '' }); const sessionQuery = useAuthSession({ enabled: !isDemoMode }); const signedIn = isDemoMode || Boolean(sessionQuery.data); const openCreate = () => { if (!signedIn) { navigate('/auth/sign-in'); return; } setCreateOpen(true); }; const projectHref = (projectId: string) => isDemoMode ? `/projects/${projectId}?demo=1` : `/projects/${projectId}`; const projectsQuery = useQuery({ queryKey: ['projects'], enabled: !isDemoMode, queryFn: () => listProjects(), }); const hostQuery = useQuery({ queryKey: ['host-monitoring'], enabled: !isDemoMode, queryFn: getHostMonitoring, refetchInterval: 30_000, }); const deploysQuery = useQuery({ queryKey: ['recent-deploys'], enabled: !isDemoMode, queryFn: () => listRecentDeployments(10), refetchInterval: 30_000, }); const createProjectMutation = useMutation({ mutationFn: () => createProject({ name: form.name.trim(), description: form.description.trim() || undefined }), onSuccess: (project) => { setCreateOpen(false); setForm({ name: '', description: '' }); queryClient.invalidateQueries({ queryKey: ['projects'] }); navigate(projectHref(project.id)); }, }); const filteredProjects = useMemo(() => { const source = isDemoMode ? demoProjects : projectsQuery.data ?? []; if (!search.trim()) return source; const needle = search.toLowerCase(); return source.filter((project) => project.name.toLowerCase().includes(needle) || project.description?.toLowerCase().includes(needle) ); }, [isDemoMode, projectsQuery.data, search]); const projects = isDemoMode ? demoProjects : projectsQuery.data ?? []; const totalServices = projects.reduce((sum, p) => sum + p.stats.service_count, 0); const runningServices = projects.reduce((sum, p) => sum + p.stats.running_services, 0); const host = hostQuery.data; const hostLabel = host?.scope === 'docker-desktop-vm' ? 'VM' : 'HOST'; const cpuPct = isDemoMode ? 14 : host && host.cpu.cores > 0 ? Math.min(100, Math.round((host.load.load1m / host.cpu.cores) * 100)) : null; const memPct = isDemoMode ? 72 : host ? Math.round(host.memory.usagePercent) : null; const diskPct = isDemoMode ? 73 : host ? Math.round(host.storage.usagePercent) : null; const fmtGB = (v: number) => `${(v / (1024 * 1024 * 1024)).toFixed(0)}G`; const deployments = isDemoMode ? [] : deploysQuery.data ?? []; const deployCount = deployments.length; const feed = isDemoMode ? demoDeploys : deployments.slice(0, 5).map((d) => ({ id: d.id, name: d.serviceName || d.imageName || d.serviceId || d.id, project: d.projectName, status: (d.status || 'queued').toUpperCase(), when: formatRelative(d.startedAt ?? d.completedAt ?? d.createdAt), })); const statusClass = (status: string) => status === 'DEPLOYED' || status === 'SUCCESS' || status === 'SUCCEEDED' ? 'v-st-ok' : status === 'BUILDING' || status === 'RUNNING' || status === 'QUEUED' ? 'v-st-run' : status === 'FAILED' || status === 'CANCELLED' ? 'v-st-fail' : 'v-st-off'; return (
{/* Page head */}

Projects_

{String(projects.length).padStart(2, '0')} projects ·{' '} {String(totalServices).padStart(2, '0')} services ·{' '} {runningServices} online

setSearch(e.target.value)} placeholder="search projects…" className="v-mono" style={{ fontSize: 11.5 }} />
{signedIn && ( )}
{/* Host stat row */}
{hostLabel}.CPU
{cpuPct ?? '—'}%
{isDemoMode ? (
{[28, 44, 36, 58, 42, 66, 50, 34, 56, 40, 30, 20].map((h, i) => ( ))}
) : null}
{isDemoMode || host ? ( <>healthy · {host?.cpu.cores ?? 16} cores ) : 'no telemetry'}
{hostLabel}.MEM
{memPct ?? '—'}%
85 ? 'r' : 'y'} style={{ width: `${memPct ?? 0}%` }} />
{isDemoMode ? '9.8G / 13G' : host ? `${fmtGB(host.memory.used)} / ${fmtGB(host.memory.total)}` : 'no telemetry'}
{hostLabel}.DISK
{diskPct ?? '—'}%
{isDemoMode ? '338G of 465G · high' : host ? `${fmtGB(host.storage.used)} of ${fmtGB(host.storage.total)}` : 'no telemetry'}
DEPLOYS
{isDemoMode ? 18 : deployCount}
{isDemoMode ? '17 ok' : `${deployments.filter((d) => (d.status ?? '').toLowerCase() !== 'failed').length} recent`}
{/* Projects grid */}

PROJECTS {filteredProjects.length} records

{!isDemoMode && projectsQuery.isLoading && (
{Array.from({ length: 6 }).map((_, i) => (
))}
)} {!isDemoMode && projectsQuery.isError && (

Failed to load projects

Check API connectivity and try again

)} {filteredProjects.length === 0 && !projectsQuery.isLoading && !projectsQuery.isError ? (

No projects yet

{signedIn ? 'Create your first project to start deploying services with visual topology management.' : 'No public projects yet. Sign in to create one — an admin approves it before it goes public.'}

) : null} {filteredProjects.length > 0 && (
{filteredProjects.map((project) => ( ))}
)} {/* Deploy feed */}

RECENT DEPLOYMENTS latest

{feed.length === 0 ? ( ) : feed.map((d) => ( ))}
SERVICESTATUSWHEN
no deployments yet
{d.name}{d.project ? {d.project} : null} {d.status} {d.when}
{/* Create Modal */} {isCreateOpen && (
setCreateOpen(false)} />

Create new project

Projects organize your services and provide a visual canvas for topology management.

New projects stay private until a platform admin approves them for public listing.

setForm((p) => ({ ...p, name: e.target.value }))} placeholder="my-awesome-project" className="h-11 w-full rounded-[var(--radius-md)] border border-[var(--border-subtle)] bg-[var(--surface-muted)] px-4 text-[var(--text-primary)] transition-all placeholder:text-[var(--text-muted)] focus:border-[var(--accent-primary)] focus:ring-1 focus:ring-[var(--accent-primary)]" />