import { useEffect, useMemo, useRef, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { createDeployment, deleteService, getProjectById, getDeploymentLogs, getServiceById, getServiceMetrics, listDeployments, listServiceLogs, listServiceVariables, listCronJobs, createCronJob, updateCronJob, deleteCronJob, listCronExecutions, triggerCronJob, listPreviewEnvironments, createPreviewEnvironment, deletePreviewEnvironment, promotePreviewEnvironment, getScalingPolicy, setScalingPolicy, deleteScalingPolicy, getServiceScalingState, manualScaleService, execInService, rollbackDeployment, cancelDeployment, updateServiceVariables, updateService, restartService, stopService, sleepService, wakeService, getServiceRuntime, getServiceEnvCheck, type CronJobEntity, type ServiceVolume, type ServiceDomain, type DomainCheckResult, listServiceDomains, addServiceDomain, removeServiceDomain, setDefaultServiceDomain, checkServiceDomains, cloneService, moveService, listProjects, } from '@/lib/api-client'; import { getDemoProjectById, getDemoServiceById, getDemoCronJobsByService } from '@/lib/demo-data'; import { useDemoMode } from '@/lib/demo-mode'; import { ServiceIcon } from '@/features/workspace/canvas/ServiceIcon'; import { serviceAccent } from '@/features/workspace/canvas/service-visuals'; import { useAuthSession } from '@/lib/use-auth-session'; import { getCurrentUserProfile } from '@/lib/api-client'; import { parseDotenv, validateVariableRows, type VariableDraft } from '../variable-utils'; import { formatBytes, formatDate, formatRelative, seededMetric } from '@/lib/time'; import { EnhancedMetricCard, LineAreaChart, DonutChart } from '@/shared/components'; import { Activity, FileText, Settings, Sliders, KeyRound, Plus, Trash2, ClipboardPaste, Save, Check, X, Loader2, Clock, Cpu, Layers, MemoryStick, Zap, Timer, Sparkles, RefreshCw, Box, Ban, Play, ChevronDown, GitPullRequest, ExternalLink, Terminal, } from 'lucide-react'; type ServiceSection = 'metrics' | 'logs' | 'config' | 'variables' | 'cron' | 'previews' | 'scaling' | 'console' | 'settings'; // Sections marked manageOnly are hidden from anonymous visitors — they // expose authenticated or owner-scoped data (logs, variables, exec). const sectionItems: Array<{ key: ServiceSection; label: string; icon: typeof Activity; manageOnly?: boolean }> = [ { key: 'metrics', label: 'Metrics', icon: Activity }, { key: 'logs', label: 'Logs', icon: FileText, manageOnly: true }, { key: 'config', label: 'Config', icon: Sliders }, { key: 'variables', label: 'Variables', icon: KeyRound, manageOnly: true }, { key: 'cron', label: 'Cron', icon: Clock }, { key: 'previews', label: 'Previews', icon: GitPullRequest }, { key: 'scaling', label: 'Scaling', icon: Layers }, { key: 'console', label: 'Console', icon: Terminal, manageOnly: true }, { key: 'settings', label: 'Settings', icon: Settings, manageOnly: true }, ]; function metricStatus(percent: number): 'good' | 'average' | 'warning' { if (percent < 50) return 'good'; if (percent < 80) return 'average'; return 'warning'; } const statusLabel = { good: 'Good', average: 'Average', warning: 'High' } as const; function StatusBadge({ status }: { status: string }) { const config = { running: { color: 'var(--success)', bg: 'var(--success-soft)', Icon: Check, animate: false }, deployed: { color: 'var(--success)', bg: 'var(--success-soft)', Icon: Check, animate: false }, failed: { color: 'var(--error)', bg: 'var(--error-soft)', Icon: X, animate: false }, building: { color: 'var(--warning)', bg: 'var(--warning-soft)', Icon: Loader2, animate: true }, pending: { color: 'var(--warning)', bg: 'var(--warning-soft)', Icon: Loader2, animate: true }, queued: { color: 'var(--warning)', bg: 'var(--warning-soft)', Icon: Clock, animate: false }, rolling_back: { color: 'var(--warning)', bg: 'var(--warning-soft)', Icon: RefreshCw, animate: true }, cancelled: { color: 'var(--text-tertiary)', bg: 'var(--surface-muted)', Icon: Ban, animate: false }, stopped: { color: 'var(--text-tertiary)', bg: 'var(--surface-muted)', Icon: Box, animate: false }, }[status] || { color: 'var(--text-tertiary)', bg: 'var(--surface-muted)', Icon: Box, animate: false }; const { Icon } = config; return (
{status.replace('_', ' ')}
); } export function ServiceDetailPage() { const { projectId = '', serviceId = '' } = useParams<{ projectId: string; serviceId: string }>(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const queryClient = useQueryClient(); const isDemoMode = useDemoMode(); const sessionQuery = useAuthSession({ enabled: !isDemoMode }); const signedIn = isDemoMode || Boolean(sessionQuery.data); const profileQuery = useQuery({ queryKey: ['user-profile'], queryFn: getCurrentUserProfile, enabled: signedIn && !isDemoMode, retry: false, }); const isAdmin = Boolean(profileQuery.data?.isAdmin); const [activeSection, setActiveSection] = useState(() => { const requested = searchParams.get('section'); return sectionItems.some((item) => item.key === requested) ? (requested as ServiceSection) : 'metrics'; }); const [logTail, setLogTail] = useState('100'); const [varDrafts, setVarDrafts] = useState(null); const [bulkOpen, setBulkOpen] = useState(false); const [bulkText, setBulkText] = useState(''); const [logDeploymentId, setLogDeploymentId] = useState(null); const projectQuery = useQuery({ queryKey: ['project', projectId], queryFn: () => getProjectById(projectId), enabled: Boolean(projectId) && !isDemoMode, }); const serviceQuery = useQuery({ queryKey: ['service', serviceId], queryFn: () => getServiceById(serviceId), enabled: Boolean(serviceId) && !isDemoMode, }); const deploymentsQuery = useQuery({ queryKey: ['service-deployments', serviceId], queryFn: () => listDeployments(serviceId), enabled: Boolean(serviceId) && !isDemoMode, refetchInterval: 4000, }); const variablesQuery = useQuery({ queryKey: ['service-variables', serviceId], queryFn: () => listServiceVariables(serviceId), enabled: Boolean(serviceId) && !isDemoMode && activeSection === 'variables', }); const envCheckQuery = useQuery({ queryKey: ['service-env-check', serviceId], queryFn: () => getServiceEnvCheck(serviceId), enabled: Boolean(serviceId) && !isDemoMode && activeSection === 'variables', }); const saveVariablesMutation = useMutation({ mutationFn: (rows: VariableDraft[]) => updateServiceVariables( serviceId, rows.map((row) => ({ key: row.key.trim(), value: row.value, is_secret: row.isSecret })), ), onSuccess: (data) => { queryClient.setQueryData(['service-variables', serviceId], data); setVarDrafts(null); }, }); // --- Cron jobs --- const [cronForm, setCronForm] = useState<{ name: string; schedule: string; command: string; timezone: string; enabled: boolean; retention: number; } | null>(null); const [editingCronId, setEditingCronId] = useState(null); const [historyCronId, setHistoryCronId] = useState(null); const cronJobsQuery = useQuery({ queryKey: ['service-cron-jobs', serviceId], queryFn: () => listCronJobs(serviceId), enabled: Boolean(serviceId) && !isDemoMode && activeSection === 'cron', }); const cronExecutionsQuery = useQuery({ queryKey: ['cron-executions', historyCronId], queryFn: () => listCronExecutions(historyCronId as string), enabled: Boolean(historyCronId) && !isDemoMode && activeSection === 'cron', }); const invalidateCron = () => queryClient.invalidateQueries({ queryKey: ['service-cron-jobs', serviceId] }); const saveCronMutation = useMutation({ mutationFn: async () => { if (!cronForm) return; const payload = { name: cronForm.name.trim(), schedule: cronForm.schedule.trim(), command: cronForm.command.trim(), timezone: cronForm.timezone.trim() || 'UTC', enabled: cronForm.enabled, retention: cronForm.retention, }; if (editingCronId) { await updateCronJob(editingCronId, payload); } else { await createCronJob({ project_id: projectId, service_id: serviceId, ...payload }); } }, onSuccess: () => { setCronForm(null); setEditingCronId(null); invalidateCron(); }, }); const deleteCronMutation = useMutation({ mutationFn: (id: string) => deleteCronJob(id), onSuccess: invalidateCron, }); const toggleCronMutation = useMutation({ mutationFn: (job: CronJobEntity) => updateCronJob(job.id as string, { enabled: !job.enabled }), onSuccess: invalidateCron, }); // --- Preview environments --- const [previewForm, setPreviewForm] = useState<{ branch: string; prNumber: string; ttlHours: string } | null>(null); const previewsQuery = useQuery({ queryKey: ['project-preview-envs', projectId], queryFn: () => listPreviewEnvironments(projectId), enabled: Boolean(projectId) && !isDemoMode && activeSection === 'previews', refetchInterval: 15_000, }); const invalidatePreviews = () => queryClient.invalidateQueries({ queryKey: ['project-preview-envs', projectId] }); const createPreviewMutation = useMutation({ mutationFn: async () => { if (!previewForm) return; await createPreviewEnvironment(projectId, { project_id: projectId, service_id: serviceId, branch_name: previewForm.branch.trim(), pr_number: previewForm.prNumber ? Number(previewForm.prNumber) : undefined, ttl_hours: previewForm.ttlHours ? Number(previewForm.ttlHours) : undefined, }); }, onSuccess: () => { setPreviewForm(null); invalidatePreviews(); }, }); const deletePreviewMutation = useMutation({ mutationFn: (id: string) => deletePreviewEnvironment(id), onSuccess: invalidatePreviews, }); const promotePreviewMutation = useMutation({ mutationFn: (id: string) => promotePreviewEnvironment(id, { target_environment: 'production' }), onSuccess: invalidatePreviews, }); const previews = (previewsQuery.data ?? []).filter((p) => p.service_id === serviceId); // --- Scaling --- const [scalingForm, setScalingForm] = useState<{ min: string; max: string; targetCpu: string; targetMemory: string; enabled: boolean; } | null>(null); const [scaleReplicas, setScaleReplicas] = useState(''); const scalingPolicyQuery = useQuery({ queryKey: ['scaling-policy', serviceId], queryFn: () => getScalingPolicy(serviceId), enabled: Boolean(serviceId) && !isDemoMode && activeSection === 'scaling', }); const scalingStateQuery = useQuery({ queryKey: ['scaling-state', serviceId], queryFn: () => getServiceScalingState(serviceId), enabled: Boolean(serviceId) && !isDemoMode && activeSection === 'scaling', refetchInterval: 15_000, }); const invalidateScaling = () => { queryClient.invalidateQueries({ queryKey: ['scaling-policy', serviceId] }); queryClient.invalidateQueries({ queryKey: ['scaling-state', serviceId] }); }; const saveScalingMutation = useMutation({ mutationFn: async () => { if (!scalingForm) return; await setScalingPolicy({ service_id: serviceId, min_replicas: Number(scalingForm.min) || 1, max_replicas: Number(scalingForm.max) || 1, target_cpu: Number(scalingForm.targetCpu) || 70, target_memory: Number(scalingForm.targetMemory) || 80, enabled: scalingForm.enabled, }); }, onSuccess: () => { setScalingForm(null); invalidateScaling(); }, }); const deleteScalingMutation = useMutation({ mutationFn: () => deleteScalingPolicy(serviceId), onSuccess: invalidateScaling, }); const manualScaleMutation = useMutation({ mutationFn: (replicas: number) => manualScaleService(serviceId, replicas), onSuccess: () => { invalidateScaling(); queryClient.invalidateQueries({ queryKey: ['service-runtime', serviceId] }); queryClient.invalidateQueries({ queryKey: ['project-services'] }); }, }); // --- Console --- const [consoleInput, setConsoleInput] = useState(''); const [consoleHistory, setConsoleHistory] = useState< Array<{ command: string; output: string; exitCode: number; error: string }> >([]); const execMutation = useMutation({ mutationFn: (command: string) => execInService(serviceId, command), onSuccess: (result, command) => { setConsoleHistory((prev) => [ { command, output: result.output ?? '', exitCode: result.exit_code ?? 0, error: result.error ?? '' }, ...prev, ]); setConsoleInput(''); }, }); const triggerCronMutation = useMutation({ mutationFn: (id: string) => triggerCronJob(id), onSuccess: () => { window.setTimeout(() => { invalidateCron(); if (historyCronId) queryClient.invalidateQueries({ queryKey: ['cron-executions', historyCronId] }); }, 2500); }, }); const serviceLogsQuery = useQuery({ queryKey: ['service-logs', serviceId, logTail], queryFn: () => listServiceLogs(serviceId, { tail: logTail }), enabled: Boolean(serviceId) && !isDemoMode && activeSection === 'logs', }); const latestDeployment = useMemo(() => { if (isDemoMode) { return null; } const deployments = deploymentsQuery.data ?? []; return deployments[0] ?? null; }, [deploymentsQuery.data, isDemoMode]); const logsDeployment = (deploymentsQuery.data ?? []).find((d) => d.id === logDeploymentId) ?? latestDeployment; const deploymentLogsQuery = useQuery({ queryKey: ['deployment-logs', logsDeployment?.id], queryFn: () => getDeploymentLogs(logsDeployment!.id, { type: 'all' }), enabled: Boolean(logsDeployment?.id) && !isDemoMode && activeSection === 'logs', }); const deployMutation = useMutation({ mutationFn: (trigger: 'manual' | 'restart') => createDeployment(serviceId, { trigger }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['service-deployments', serviceId] }); queryClient.invalidateQueries({ queryKey: ['service', serviceId] }); queryClient.invalidateQueries({ queryKey: ['project-services', projectId] }); }, }); const cancelDeploymentMutation = useMutation({ mutationFn: (deploymentId: string) => cancelDeployment(deploymentId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['service-deployments', serviceId] }); queryClient.invalidateQueries({ queryKey: ['service', serviceId] }); }, }); const rollbackMutation = useMutation({ mutationFn: (deploymentId: string) => rollbackDeployment(deploymentId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['service-deployments', serviceId] }); queryClient.invalidateQueries({ queryKey: ['service', serviceId] }); queryClient.invalidateQueries({ queryKey: ['project-services', projectId] }); }, }); const deleteServiceMutation = useMutation({ mutationFn: () => deleteService(serviceId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['project-services', projectId] }); queryClient.invalidateQueries({ queryKey: ['projects'] }); navigate(isDemoMode ? `/projects/${projectId}?demo=1` : `/projects/${projectId}`); }, }); const projectsQuery = useQuery({ queryKey: ['projects'], queryFn: () => listProjects({ limit: 100 }), enabled: !isDemoMode && activeSection === 'settings', }); const cloneMutation = useMutation({ mutationFn: (input: { name?: string; project_id?: string }) => cloneService(serviceId, input), onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ['project-services'] }); const targetProject = lifecycleForm?.cloneProject || projectId; navigate(`/projects/${targetProject}/services/${data.service_id}`); }, }); const moveMutation = useMutation({ mutationFn: (targetProjectId: string) => moveService(serviceId, targetProjectId), onSuccess: (_data, targetProjectId) => { queryClient.invalidateQueries({ queryKey: ['project-services'] }); queryClient.invalidateQueries({ queryKey: ['projects'] }); navigate(`/projects/${targetProjectId}/services/${serviceId}`); }, }); const [lifecycleForm, setLifecycleForm] = useState<{ cloneName: string; cloneProject: string; moveTarget: string; } | null>(null); const invalidateService = () => { queryClient.invalidateQueries({ queryKey: ['service', serviceId] }); queryClient.invalidateQueries({ queryKey: ['service-runtime', serviceId] }); queryClient.invalidateQueries({ queryKey: ['project-services', projectId] }); }; const restartMutation = useMutation({ mutationFn: () => restartService(serviceId), onSuccess: invalidateService, }); const stopMutation = useMutation({ mutationFn: () => stopService(serviceId), onSuccess: invalidateService, }); const sleepMutation = useMutation({ mutationFn: () => sleepService(serviceId), onSuccess: invalidateService, }); const wakeMutation = useMutation({ mutationFn: () => wakeService(serviceId), onSuccess: invalidateService, }); const updateServiceMutation = useMutation({ mutationFn: (input: Parameters[1]) => updateService(serviceId, input), onSuccess: () => { setNetworkForm(null); setSleepDraft(null); invalidateService(); }, }); const runtimeQuery = useQuery({ queryKey: ['service-runtime', serviceId], queryFn: () => getServiceRuntime(serviceId), enabled: Boolean(serviceId) && !isDemoMode, refetchInterval: 10_000, }); const [networkForm, setNetworkForm] = useState<{ port: string; domain: string; healthcheckPath: string; restartPolicy: string; replicas: string; } | null>(null); const [volumesForm, setVolumesForm] = useState(null); const [domainInput, setDomainInput] = useState(''); const [domainChecks, setDomainChecks] = useState | null>(null); const [accessForm, setAccessForm] = useState<{ maintenance: boolean; basicAuth: string } | null>(null); const [sleepDraft, setSleepDraft] = useState<{ id: string; enabled: boolean; minutes: string } | null>(null); const [buildForm, setBuildForm] = useState<{ builder: string; cpuReserve: string; memoryReserve: string; staticCmd: string; staticDir: string; } | null>(null); const domainsQuery = useQuery({ queryKey: ['service-domains', serviceId], queryFn: () => listServiceDomains(serviceId), enabled: Boolean(serviceId) && !isDemoMode, }); const domainMutation = useMutation({ mutationFn: (fn: () => Promise) => fn(), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['service-domains', serviceId] }); invalidateService(); }, }); const project = isDemoMode ? getDemoProjectById(projectId) : projectQuery.data; const service = isDemoMode ? getDemoServiceById(serviceId) : serviceQuery.data; const sleepDraftCurrent = sleepDraft?.id === serviceId ? sleepDraft : null; const sleepEnabled = sleepDraftCurrent?.enabled ?? Boolean(service?.sleepEnabled); const sleepMinutes = sleepDraftCurrent?.minutes ?? String(service?.sleepIdleMinutes ?? 15); const varRows: VariableDraft[] = useMemo( () => varDrafts ?? (variablesQuery.data ?? []).map((v) => ({ key: v.key, value: v.value, isSecret: v.isSecret })), [varDrafts, variablesQuery.data], ); const varErrors = useMemo(() => validateVariableRows(varRows), [varRows]); const varDirty = varDrafts !== null; const updateVarRow = (index: number, patch: Partial) => setVarDrafts(varRows.map((row, i) => (i === index ? { ...row, ...patch } : row))); const removeVarRow = (index: number) => setVarDrafts(varRows.filter((_, i) => i !== index)); const applyBulkPaste = () => { const parsed = parseDotenv(bulkText); if (parsed.length === 0) return; const merged = [...varRows]; for (const row of parsed) { const existing = merged.findIndex((r) => r.key === row.key); if (existing >= 0) { merged[existing] = { ...merged[existing], value: row.value }; } else { merged.push(row); } } setVarDrafts(merged); setBulkText(''); setBulkOpen(false); }; const metricsQuery = useQuery({ queryKey: ['service-metrics', serviceId], queryFn: () => getServiceMetrics(serviceId), enabled: !isDemoMode && Boolean(serviceId), refetchInterval: 5000, }); // Accumulate live samples into a small history for the timeline charts. const [metricHistory, setMetricHistory] = useState<{ cpu: number[]; memory: number[]; net: number[] }>({ cpu: [], memory: [], net: [] }); const lastNetSampleRef = useRef<{ rx: number; at: number } | null>(null); useEffect(() => { const sample = metricsQuery.data; if (!sample || sample.status !== 'ok') { return; } const memPercent = sample.memory_limit_bytes > 0 ? (sample.memory_usage_bytes / sample.memory_limit_bytes) * 100 : 0; const now = Date.now(); const previous = lastNetSampleRef.current; const elapsedSec = previous ? Math.max(1, (now - previous.at) / 1000) : 0; const rxRate = previous && elapsedSec > 0 ? Math.max(0, (sample.network_rx_bytes - previous.rx) / elapsedSec) : 0; lastNetSampleRef.current = { rx: sample.network_rx_bytes, at: now }; // eslint-disable-next-line react-hooks/set-state-in-effect -- accumulating polled samples is a legitimate sync-to-external pattern setMetricHistory((prev) => ({ cpu: [...prev.cpu, Math.min(100, sample.cpu_percent)].slice(-24), memory: [...prev.memory, Math.min(100, memPercent)].slice(-24), net: [...prev.net, rxRate].slice(-24), })); }, [metricsQuery.data]); const liveMetrics = isDemoMode ? null : metricsQuery.data ?? null; const hasLiveTelemetry = Boolean(liveMetrics && liveMetrics.status === 'ok' && liveMetrics.instances.length > 0); const runningInstances = liveMetrics?.instances.filter((instance) => instance.state === 'running').length ?? 0; const memoryPercent = liveMetrics && liveMetrics.memory_limit_bytes > 0 ? (liveMetrics.memory_usage_bytes / liveMetrics.memory_limit_bytes) * 100 : 0; // Demo mode renders seeded values so the layout can be previewed without a backend. const metricSet = useMemo(() => { if (!service || !isDemoMode) { return null; } return { cpu: seededMetric(`${service.id}:cpu`, 12, 78), memory: seededMetric(`${service.id}:mem`, 24, 91), req: seededMetric(`${service.id}:req`, 120, 5100), latency: seededMetric(`${service.id}:lat`, 17, 210), }; }, [service, isDemoMode]); const deploymentSummary = useMemo(() => { const deployments = isDemoMode ? [] : deploymentsQuery.data ?? []; const total = deployments.length; const active = deployments.filter((deployment) => ['queued', 'pending', 'building', 'deploying', 'rolling_back'].includes(deployment.status), ).length; const failed = deployments.filter((deployment) => deployment.status === 'failed').length; return { total, active, failed }; }, [deploymentsQuery.data, isDemoMode]); const canRollback = (status: string): boolean => status === 'deployed' || status === 'failed'; if (!isDemoMode && (projectQuery.isLoading || serviceQuery.isLoading)) { return (
Loading service...
); } if (!isDemoMode && serviceQuery.isError) { return (

Failed to load service

{(serviceQuery.error as Error).message}

); } if (!service || !project) { return (

Service not found

This service may have been deleted.

); } return (
{/* Breadcrumb - self.html exact match */}
/ {service.name}
{/* Project Header - self.html exact match */}
{service.name} {service.status === 'running' && } {service.status === 'running' ? 'Active' : service.status === 'degraded' ? 'Degraded' : service.status === 'sleeping' ? 'Sleeping' : 'Stopped'}
{(service.publicUrl || service.domain) && ( {service.publicUrl ?? `https://${service.domain}`} )}
{!isDemoMode && signedIn && ( <> )}
{/* Demo Mode Banner */} {isDemoMode && (
Demo mode active — using sample data for preview
)} {/* Metrics Overview */}
} value={isDemoMode ? `${metricSet?.cpu ?? 0}%` : hasLiveTelemetry ? `${liveMetrics!.cpu_percent.toFixed(1)}%` : '—'} status={metricStatus(isDemoMode ? metricSet?.cpu ?? 0 : liveMetrics?.cpu_percent ?? 0)} statusText={isDemoMode || hasLiveTelemetry ? statusLabel[metricStatus(isDemoMode ? metricSet?.cpu ?? 0 : liveMetrics?.cpu_percent ?? 0)] : ''} subtitle={isDemoMode ? 'Current utilization' : hasLiveTelemetry ? `${runningInstances} running container${runningInstances === 1 ? '' : 's'}` : 'No running containers'} chart={ seededMetric(`${service.id}:cpu:${i}`, 8, 80)) : metricHistory.cpu.length > 0 ? metricHistory.cpu : [0]} color="var(--accent-primary)" height={72} /> } /> } value={isDemoMode ? `${metricSet?.memory ?? 0}%` : hasLiveTelemetry ? `${memoryPercent.toFixed(0)}%` : '—'} status={metricStatus(isDemoMode ? metricSet?.memory ?? 0 : memoryPercent)} statusText={isDemoMode || hasLiveTelemetry ? statusLabel[metricStatus(isDemoMode ? metricSet?.memory ?? 0 : memoryPercent)] : ''} subtitle={isDemoMode ? 'Container footprint' : hasLiveTelemetry ? `${formatBytes(liveMetrics!.memory_usage_bytes)} used` : 'No running containers'} chart={
Used
{isDemoMode ? `${((8 * (metricSet?.memory ?? 0)) / 100).toFixed(1)} GB / 8 GB` : hasLiveTelemetry ? `${formatBytes(liveMetrics!.memory_usage_bytes)} / ${formatBytes(liveMetrics!.memory_limit_bytes)}` : '—'}
} /> } value={isDemoMode ? `${metricSet?.req ?? 0}` : hasLiveTelemetry ? formatBytes(liveMetrics!.network_rx_bytes) : '—'} status="good" statusText={isDemoMode || hasLiveTelemetry ? 'RX' : ''} subtitle={isDemoMode ? 'Requests, last 60 minutes' : hasLiveTelemetry ? `TX ${formatBytes(liveMetrics!.network_tx_bytes)}` : 'No traffic recorded'} chart={ seededMetric(`${service.id}:net:${i}`, 15, 70)) : metricHistory.net.length > 0 ? metricHistory.net : [0]} color="var(--info)" height={72} /> } /> } value={isDemoMode ? `${metricSet?.latency ?? 0}ms` : hasLiveTelemetry ? `${runningInstances}` : '0'} statusText={hasLiveTelemetry || isDemoMode ? 'Running' : ''} status="good" subtitle={isDemoMode ? 'P95 latency estimate' : hasLiveTelemetry ? `${liveMetrics!.instances.length} container${liveMetrics!.instances.length === 1 ? '' : 's'} discovered` : 'Deploy to create containers'} />
{/* Deployment Summary */} {!isDemoMode && (
{deploymentSummary.total} deployments
{deploymentSummary.active > 0 && (
{deploymentSummary.active} active
)} {deploymentSummary.failed > 0 && (
{deploymentSummary.failed} failed
)} {latestDeployment?.createdAt && (
Latest {formatRelative(latestDeployment.createdAt)}
)}
)}
{/* Tabs - self.html exact match */}
{sectionItems.filter((item) => signedIn || !item.manageOnly).map((item) => { const active = activeSection === item.key; const Icon = item.icon; return ( ); })}
{/* Content - self.html exact match: padding 0 24px 28px */}
{activeSection === 'metrics' && (
{/* Metrics Grid with enhanced cards */} {!isDemoMode && !hasLiveTelemetry && (

No telemetry yet

{liveMetrics?.status === 'docker_unavailable' ? 'Docker is not reachable on this host, so container metrics cannot be collected.' : 'Metrics appear once this service has running containers. Deploy it to start collecting.'}

)} {(isDemoMode || hasLiveTelemetry) && (

CPU Timeline

{isDemoMode ? 'Last 24 intervals' : 'Live samples · every 5s'}

{isDemoMode ? Array.from({ length: 24 }).map((_, i) => { const value = seededMetric(`${service.id}:cpu:${i}`, 8, 80); return (
); }) : metricHistory.cpu.map((value, i) => (
))} {!isDemoMode && metricHistory.cpu.length === 0 && (

Collecting first sample...

)}

Memory Timeline

{isDemoMode ? 'Last 24 intervals' : 'Live samples · every 5s'}

{isDemoMode ? Array.from({ length: 24 }).map((_, i) => { const value = seededMetric(`${service.id}:req:${i}`, 18, 96); return (
); }) : metricHistory.memory.map((value, i) => (
))} {!isDemoMode && metricHistory.memory.length === 0 && (

Collecting first sample...

)}
)} {/* Instance list — real container states */} {!isDemoMode && hasLiveTelemetry && (

Instances

Containers backing this service

{liveMetrics!.instances.map((instance) => (
{instance.name}
{instance.cpu_percent.toFixed(1)}% CPU {formatBytes(instance.memory_usage_bytes)} {instance.started_at ? `up ${formatRelative(instance.started_at).replace(' ago', '')}` : instance.state}
))}
)} {/* Domain & Networking Panel */} {service.status === 'running' && (

Networking

Public endpoints and ports

{runtimeQuery.data?.health === 'healthy' && ( HEALTHY )} {runtimeQuery.data?.health === 'unhealthy' && ( UNHEALTHY )}
{service.domain && (
)} {(runtimeQuery.data?.urls ?? []).map((url) => (
))}
Internal
{service.name}{service.port ? `:${service.port}` : ''}
{!service.domain && (runtimeQuery.data?.urls ?? []).length === 0 && (

No public endpoint. Publish a port or set a domain in Settings → Networking to expose this service.

)}
)}
)} {activeSection === 'logs' && (

Service Logs

Container stdout/stderr output

{!isDemoMode && (
)}
{isDemoMode ? (
{Array.from({ length: 15 }).map((_, i) => (

[{service.updatedAt ? new Date(new Date(service.updatedAt).getTime() - i * 45000).toLocaleTimeString() : '--:--:--'}] {' '} {service.name} {' '} {i % 3 === 0 ? 'health_check=ok' : 'request=200'}

))}
) : serviceLogsQuery.isLoading ? (

Loading logs...

) : serviceLogsQuery.isError ? (

Failed to load logs.

) : (serviceLogsQuery.data?.length ?? 0) === 0 ? (

No logs available.

) : (
{serviceLogsQuery.data!.map((entry, i) => (

[{entry.timestamp ? new Date(entry.timestamp).toLocaleTimeString() : '--:--:--'}] {' '} {entry.stream} {' '} {entry.message}

))}
)}
{!isDemoMode && logsDeployment && (

{logsDeployment.id === latestDeployment?.id ? 'Latest Deployment' : 'Deployment'}

{logsDeployment.id !== latestDeployment?.id && ( )}

{logsDeployment.id}

                  {deploymentLogsQuery.isLoading
                    ? 'Loading...'
                    : deploymentLogsQuery.isError
                    ? 'Failed to load.'
                    : deploymentLogsQuery.data?.buildLog || deploymentLogsQuery.data?.runtimeLog || '(no output)'}
                
)}
)} {activeSection === 'config' && (

Configuration

Runtime and deployment settings

Environment

{service.environment ?? 'production'}

Image

{service.image ?? 'not set'}

Command

{service.command ?? 'default'}

Git Branch

{service.gitBranch ?? 'not configured'}

)} {activeSection === 'variables' && (

Environment Variables

Runtime values injected at deploy. Reference another service's variable with{' '} {'${{service.KEY}}'} or a project shared variable with{' '} {'${{shared.KEY}}'}

{envCheckQuery.data && !envCheckQuery.data.ok && (

Environment issues detected

    {(envCheckQuery.data.unresolved ?? []).map((u) => (
  • Unresolved reference: {u}
  • ))} {(envCheckQuery.data.empty ?? []).map((k) => (
  • Empty value: {k}
  • ))} {(envCheckQuery.data.unreadable ?? []).map((k) => (
  • Cannot decrypt secret (key rotated?): {k}
  • ))}
)} {bulkOpen && (