'use client'; import { useState } from 'react'; import Link from 'next/link'; import useSWR from 'swr'; import { Icon } from './Icons'; import { DeploymentLogs } from './DeploymentLogs'; import { Deployment, DeploymentStatus, STATUS_LABELS, formatDuration, formatRelativeTime, formatDate, } from '@/lib/deployments'; import { clientConfig } from '@/lib/config'; // SWR fetcher const fetcher = (url: string) => fetch(url).then((r) => r.json()); // Types for health and stats responses type HealthStatus = 'healthy' | 'unhealthy' | 'starting' | 'none' | 'unknown'; interface HealthResponse { status: HealthStatus; failingStreak: number; log: { start: string; end: string; exitCode: number; output: string }[]; containerName: string | null; lastCheck: string | null; } interface StatsResponse { containerName: string | null; stats: { cpuPercent: number; memoryUsage: string; memoryLimit: string; memoryPercent: number; netIO: { rx: string; tx: string }; blockIO: { read: string; write: string }; } | null; uptime: { startedAt: string | null; seconds: number; formatted: string; } | null; timestamp: string; } interface DeploymentDashboardProps { deployment: Deployment; } type TabId = 'deployment' | 'logs' | 'resources' | 'source'; const tabs: { id: TabId; label: string }[] = [ { id: 'deployment', label: 'Deployment' }, { id: 'logs', label: 'Logs' }, { id: 'resources', label: 'Resources' }, { id: 'source', label: 'Source' }, ]; // Status badge component similar to Vercel's design const StatusBadge = ({ status, showLabel = true }: { status: DeploymentStatus; showLabel?: boolean }) => { const dotColors: Record = { finished: 'bg-emerald-500', error: 'bg-red-500', in_progress: 'bg-amber-500 animate-pulse', queued: 'bg-slate-400', cancelled: 'bg-slate-400', }; return (
{showLabel && ( {STATUS_LABELS[status]} )}
); }; // Collapsible section component interface CollapsibleSectionProps { icon: string; iconColor?: string; title: string; badge?: React.ReactNode; rightContent?: React.ReactNode; children: React.ReactNode; defaultOpen?: boolean; } function CollapsibleSection({ icon, iconColor = 'text-amber-500', title, badge, rightContent, children, defaultOpen = false, }: CollapsibleSectionProps) { const [isOpen, setIsOpen] = useState(defaultOpen); return (
{isOpen &&
{children}
}
); } // Action card component similar to Vercel's bottom cards interface ActionCardProps { icon: string; title: string; description: string; badge?: string; badgeColor?: string; disabled?: boolean; disabledReason?: string; loading?: boolean; onClick?: () => void; href?: string; isExternal?: boolean; } function ActionCard({ icon, title, description, badge, badgeColor = 'bg-cyan-100 dark:bg-cyan-900/30 text-cyan-700 dark:text-cyan-400', disabled = false, disabledReason, loading = false, onClick, href, isExternal = true, }: ActionCardProps) { const isDisabled = disabled || loading; const isClickable = (href || onClick) && !isDisabled; const content = (
{title}
{badge && ( {badge} )} {isExternal && href && !isDisabled && ( )}

{loading ? 'Triggering deployment...' : description}

); if (href && !isDisabled) { return ( {content} ); } if (onClick && !isDisabled) { return ; } return content; } // Metadata row component function MetadataRow({ label, children, }: { label: string; children: React.ReactNode; }) { return (
{label}
{children}
); } // Health indicator component with colored dot function HealthIndicator({ status, isLoading, deploymentStatus, }: { status: HealthStatus | undefined; isLoading: boolean; deploymentStatus?: DeploymentStatus; }) { if (isLoading) { return (
checking...
); } // Handle building/queued deployments that don't have containers yet if (deploymentStatus === 'in_progress' || deploymentStatus === 'queued') { return (
{deploymentStatus === 'in_progress' ? 'Building...' : 'Pending'}
); } // Handle failed deployments if (deploymentStatus === 'error') { return (
Failed
); } // Handle cancelled deployments if (deploymentStatus === 'cancelled') { return (
Cancelled
); } const statusConfig: Record = { healthy: { color: 'bg-emerald-500', label: 'Healthy' }, unhealthy: { color: 'bg-red-500', label: 'Unhealthy' }, starting: { color: 'bg-amber-500 animate-pulse', label: 'Starting' }, none: { color: 'bg-slate-400 dark:bg-stone-500', label: 'No healthcheck' }, unknown: { color: 'bg-slate-400 dark:bg-stone-500', label: 'Unknown' }, }; const config = statusConfig[status || 'unknown']; return (
{config.label}
); } // Stats display component function StatsDisplay({ stats, uptime, containerName, isLoading, deploymentStatus, }: { stats: StatsResponse['stats']; uptime: StatsResponse['uptime']; containerName: string | null; isLoading: boolean; deploymentStatus?: DeploymentStatus; }) { if (isLoading) { return (
{[...Array(4)].map((_, i) => (
))}
); } // Handle different deployment statuses when no stats if (!stats && !uptime) { // Deployment is building/queued - container not started yet if (deploymentStatus === 'in_progress' || deploymentStatus === 'queued') { return (
Container not started yet - deployment in progress
); } // Deployment failed - container may never have started if (deploymentStatus === 'error') { return (
Deployment failed - no container available
); } // Deployment cancelled if (deploymentStatus === 'cancelled') { return (
Deployment cancelled - no container available
); } // Default: container not found (may be stopped or removed) return (
Container not found or not running
); } return (
{/* CPU */}
CPU
{stats ? `${stats.cpuPercent.toFixed(1)}%` : '-'}
{/* Memory */}
Memory
{stats ? stats.memoryUsage : '-'} {stats && ( ({stats.memoryPercent.toFixed(1)}%) )}
{/* Uptime */}
Uptime
{uptime ? uptime.formatted : '-'}
{/* Container */}
Container
{containerName ? containerName.substring(0, 20) : '-'}
); } export function DeploymentDashboard({ deployment }: DeploymentDashboardProps) { const [activeTab, setActiveTab] = useState('deployment'); // Auto-expand logs for error deployments const [logsOpen, setLogsOpen] = useState(true); const [isRedeploying, setIsRedeploying] = useState(false); // Check if deployment is in a terminal error state const isErrorState = deployment.status === 'error'; const isInProgress = deployment.status === 'in_progress' || deployment.status === 'queued'; const isFinished = deployment.status === 'finished'; const isCancelled = deployment.status === 'cancelled'; const handleRedeploy = async () => { if (!confirm('Trigger a new deployment?')) return; setIsRedeploying(true); try { const res = await fetch(`/api/deployments/${deployment.deployment_uuid}/redeploy`, { method: 'POST', }); if (res.ok) { alert('Deployment triggered!'); } else { const data = await res.json(); alert(`Failed to trigger deployment: ${data.error || 'Unknown error'}`); } } catch (error) { alert(`Failed to trigger deployment: ${error}`); } finally { setIsRedeploying(false); } }; // SWR hooks for real-time health and stats data const { data: health, isLoading: healthLoading } = useSWR( `/api/deployments/${deployment.deployment_uuid}/health`, fetcher, { refreshInterval: 10000 } ); const { data: stats, isLoading: statsLoading } = useSWR( `/api/deployments/${deployment.deployment_uuid}/stats`, fetcher, { refreshInterval: 10000 } ); // Preview image URL (ImageResponse generates PNG directly) const previewUrl = `/api/deployments/${deployment.deployment_uuid}/preview`; const coolifyUrl = `${clientConfig.coolifyUrl}/project/${clientConfig.coolifyProjectUuid}/production/application/${deployment.application_uuid}/deployment/${deployment.deployment_uuid}`; // Dozzle URL for runtime logs (uses IP since no domain configured) // Format: http://host:9999/container/{hostId}~{containerName} const dozzleUrl = stats?.containerName ? `http://192.168.1.3:9999/container/${clientConfig.dozzleHostId}~${stats.containerName}` : null; // Count warnings in logs (simple heuristic) const warningCount = deployment.logs ? (deployment.logs.match(/warning|warn/gi) || []).length : 0; return (
{/* Header section with app name and actions */}

{deployment.application_name}

{deployment.deployment_uuid.substring(0, 9)}
{/* Action buttons - Vercel style */}
{deployment.application_fqdn && ( Visit )}
{/* Tab navigation (Vercel style) */}
{/* Tab content */} {activeTab === 'deployment' && (
{/* Error banner for failed deployments */} {isErrorState && (

Deployment Failed

Check the build logs below for error details.

)} {/* In-progress banner */} {isInProgress && (

{deployment.status === 'queued' ? 'Deployment Queued' : 'Deployment in Progress'}

{deployment.status === 'queued' ? 'Waiting for available resources...' : 'Building and deploying your application...'}

)} {/* Deployment Details Header - Vercel style */}

Deployment Details

{deployment.application_fqdn && ( Visit )}
{/* Main content: Preview + Metadata Grid */}
{/* Preview thumbnail (left side) - generated via ImageResponse */}
{`Preview
{/* Metadata grid (right side) - 2 columns like Vercel */}
{/* Created */}
{formatDate(deployment.created_at)}
{/* Status */}
{deployment.is_current && ( Latest )}
{/* Health */} {/* Duration */}
{deployment.duration != null ? formatDuration(deployment.duration) : isInProgress ? 'In progress...' : '-'} {deployment.created_at ? formatRelativeTime(deployment.created_at) : ''}
{/* Environment */}
Production {deployment.is_current && ( Current )}
{/* Domains */} {deployment.application_fqdn ? ( {deployment.application_fqdn.replace(/^https?:\/\//, '')} ) : ( No domain configured )} {/* Source */}
{deployment.git_branch || '—'} {deployment.git_commit_sha?.substring(0, 7) || '—'}

{deployment.commit_message || 'No commit message'}

{/* Collapsible Sections - Vercel style */} Inherited } >
Settings inherited from application configuration.
{warningCount > 0 && ( {warningCount} )} {deployment.duration && ( {formatDuration(deployment.duration)} )}
} > {stats?.stats && ( CPU: {stats.stats.cpuPercent.toFixed(1)}% )}
} >
Resources
} >
{isErrorState && 'Deployment failed. Check the build logs above for details.'} {isInProgress && 'Deployment is in progress. Container will be available once build completes.'} {isCancelled && 'Deployment was cancelled.'} {isFinished && 'Deployment completed successfully. Container is running.'}
{/* Action Cards Grid - Vercel style */}
)} {activeTab === 'logs' && (

Build Logs

)} {activeTab === 'resources' && (

Resources

{/* Stats Grid */} {/* Additional Network & Disk I/O */} {stats?.stats && (

Network I/O

Received {stats.stats.netIO.rx}
Transmitted {stats.stats.netIO.tx}

Block I/O

Read {stats.stats.blockIO.read}
Write {stats.stats.blockIO.write}
)} {/* Health History */} {health?.log && health.log.length > 0 && (

Health Check History

{health.log.slice(-5).reverse().map((entry, i) => (
{new Date(entry.end).toLocaleTimeString()} {entry.output.trim() || (entry.exitCode === 0 ? 'OK' : 'Failed')}
))}
)}
)} {activeTab === 'source' && (

Source

{deployment.git_branch || '—'}

{deployment.git_commit_sha ? deployment.git_commit_sha.substring(0, 7) : '—'}

{deployment.commit_message || 'No commit message'}

{deployment.is_webhook && ( Triggered by webhook )} {deployment.is_api && ( Triggered by API )} {!deployment.is_webhook && !deployment.is_api && ( Manual trigger )}
)} {/* Link to Coolify */}
View in Coolify
); }