'use client'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useRouter } from 'next/navigation'; import api from '@/lib/api'; import { toast } from 'react-toastify'; import type { Application, Deployment, ResourceUsage, ClusterPublic, ClusterPoolPublic } from '@/types'; import { useState, useRef, useCallback, useEffect } from 'react'; import { Hexagon, Rocket, Play, Square, RotateCw, Globe, Package, Server, Scale, CheckCircle, Clock, XCircle, AlertCircle, Link, GitBranch, KeyRound, FolderUp, BarChart3, ChevronDown, FileText, Monitor, Hammer, Settings, RefreshCw, Upload, Circle, Pin } from 'lucide-react'; const statusColors: Record = { running: 'badge-green', pending: 'badge-yellow', building: 'badge-blue', deploying: 'badge-blue', failed: 'badge-red', build_failed: 'badge-red', stopped: 'badge-gray', }; /** * Parse CPU value to millicores (e.g. "100m" → 100, "1" → 1000, "250n" → 0.00025) */ function parseCpuToMillicores(cpu: string): number { if (!cpu || cpu === '0') return 0; if (cpu.endsWith('n')) return parseFloat(cpu) / 1_000_000; if (cpu.endsWith('u')) return parseFloat(cpu) / 1_000; if (cpu.endsWith('m')) return parseFloat(cpu); return parseFloat(cpu) * 1000; } /** * Parse memory value to MiB (e.g. "128Mi" → 128, "1Gi" → 1024, "131072Ki" → 128) */ function parseMemoryToMi(memory: string): number { if (!memory || memory === '0') return 0; if (memory.endsWith('Ki')) return parseFloat(memory) / 1024; if (memory.endsWith('Mi')) return parseFloat(memory); if (memory.endsWith('Gi')) return parseFloat(memory) * 1024; if (memory.endsWith('Ti')) return parseFloat(memory) * 1024 * 1024; // raw bytes return parseFloat(memory) / (1024 * 1024); } export default function AppDetailPage() { const params = useParams(); const router = useRouter(); const queryClient = useQueryClient(); const appId = params.id as string; const [showLogs, setShowLogs] = useState(false); const [logTab, setLogTab] = useState<'pod' | 'build'>('pod'); const fileInputRef = useRef(null); const logsEndRef = useRef(null); const [uploadProgress, setUploadProgress] = useState(0); const [isDragging, setIsDragging] = useState(false); const [showResources, setShowResources] = useState(false); const [resourceForm, setResourceForm] = useState({ cpuRequest: '', cpuLimit: '', memoryRequest: '', memoryLimit: '', replicas: 1, }); const { data: app, isLoading } = useQuery({ queryKey: ['application', appId], queryFn: () => api.get(`/applications/${appId}`).then((r) => r.data), }); const { data: deployments = [] } = useQuery({ queryKey: ['deployments', appId], queryFn: () => api.get(`/deployments/applications/${appId}`).then((r) => r.data), refetchInterval: 5000, // Poll for status updates }); const { data: logsData } = useQuery<{ logs: string }>({ queryKey: ['logs', appId], queryFn: () => api.get(`/deployments/applications/${appId}/logs`).then((r) => r.data), enabled: showLogs && logTab === 'pod', refetchInterval: showLogs && logTab === 'pod' ? 3000 : false, }); const { data: buildLogsData } = useQuery<{ buildLog: string | null; status: string; version: string | null }>({ queryKey: ['build-logs', appId], queryFn: () => api.get(`/deployments/applications/${appId}/build-logs`).then((r) => r.data), enabled: showLogs && logTab === 'build', refetchInterval: showLogs && logTab === 'build' ? 5000 : false, }); const { data: resourceUsage, isLoading: resourcesLoading } = useQuery({ queryKey: ['resources', appId], queryFn: () => api.get(`/applications/${appId}/resources`).then((r) => r.data), enabled: showResources, refetchInterval: showResources ? 5000 : false, }); const { data: clusters = [] } = useQuery({ queryKey: ['clusters-public'], queryFn: () => api.get('/clusters/public').then((r) => r.data), }); const { data: pools = [] } = useQuery({ queryKey: ['pools-public'], queryFn: () => api.get('/clusters/pools/public').then((r) => r.data), }); // Sync form when resource data loads useEffect(() => { if (resourceUsage?.configured) { setResourceForm({ cpuRequest: resourceUsage.configured.cpuRequest, cpuLimit: resourceUsage.configured.cpuLimit, memoryRequest: resourceUsage.configured.memoryRequest, memoryLimit: resourceUsage.configured.memoryLimit, replicas: resourceUsage.configured.replicas, }); } }, [resourceUsage?.configured]); // Auto-scroll logs to bottom useEffect(() => { if (logsEndRef.current) { logsEndRef.current.scrollTop = logsEndRef.current.scrollHeight; } }, [logsData]); const invalidateAll = () => { queryClient.invalidateQueries({ queryKey: ['deployments', appId] }); queryClient.invalidateQueries({ queryKey: ['application', appId] }); }; const deployMutation = useMutation({ mutationFn: () => api.post(`/deployments/applications/${appId}/deploy`), onSuccess: () => { invalidateAll(); toast.success('Deployment triggered!'); }, onError: () => toast.error('Failed to trigger deployment'), }); const stopMutation = useMutation({ mutationFn: () => api.post(`/deployments/applications/${appId}/stop`), onSuccess: () => { invalidateAll(); toast.success('Application stopped'); }, onError: () => toast.error('Failed to stop application'), }); const startMutation = useMutation({ mutationFn: () => api.post(`/deployments/applications/${appId}/start`), onSuccess: () => { invalidateAll(); toast.success('Application started'); }, onError: () => toast.error('Failed to start application'), }); const restartMutation = useMutation({ mutationFn: () => api.post(`/deployments/applications/${appId}/restart`), onSuccess: () => { invalidateAll(); toast.success('Application restarting...'); }, onError: () => toast.error('Failed to restart application'), }); const redeployMutation = useMutation({ mutationFn: () => api.post(`/deployments/applications/${appId}/redeploy`), onSuccess: () => { invalidateAll(); toast.success('Redeploy triggered — building new version from latest source'); }, onError: () => toast.error('Failed to trigger redeploy'), }); const deleteMutation = useMutation({ mutationFn: () => api.delete(`/applications/${appId}`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['applications'] }); toast.success('Application deleted'); router.push('/dashboard/apps'); }, onError: () => toast.error('Failed to delete application'), }); const scaleMutation = useMutation({ mutationFn: (data: { cpuRequest?: string; cpuLimit?: string; memoryRequest?: string; memoryLimit?: string; replicas?: number }) => api.patch(`/applications/${appId}/resources`, data), onSuccess: () => { invalidateAll(); queryClient.invalidateQueries({ queryKey: ['resources', appId] }); toast.success('Resources updated successfully!'); }, onError: () => toast.error('Failed to update resources'), }); const previewMutation = useMutation({ mutationFn: () => api.get(`/applications/${appId}/preview`).then((r) => r.data), onSuccess: (data: { url: string; nodePort: number; host: string; ingressUrl?: string }) => { // Open the preview URL in a new tab window.open(data.url, '_blank'); toast.success(`Preview opened on port ${data.nodePort}`); }, onError: () => toast.error('Failed to get preview URL. Make sure the app is deployed.'), }); const uploadMutation = useMutation({ mutationFn: (file: File) => { const formData = new FormData(); formData.append('file', file); return api.post(`/applications/${appId}/upload`, formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (e) => { if (e.total) setUploadProgress(Math.round((e.loaded * 100) / e.total)); }, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['application', appId] }); toast.success('Source code uploaded successfully!'); setUploadProgress(0); }, onError: () => { toast.error('Failed to upload source code'); setUploadProgress(0); }, }); const handleFileUpload = useCallback((file: File) => { if (!file.name.endsWith('.zip') && !file.name.endsWith('.tar.gz') && !file.name.endsWith('.tgz')) { toast.error('Please upload a .zip or .tar.gz file'); return; } if (file.size > 100 * 1024 * 1024) { toast.error('File size must be less than 100MB'); return; } uploadMutation.mutate(file); }, [uploadMutation]); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const file = e.dataTransfer.files[0]; if (file) handleFileUpload(file); }, [handleFileUpload]); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }, []); const handleDragLeave = useCallback(() => { setIsDragging(false); }, []); if (isLoading || !app) { return (
{[1,2,3,4,5].map(i =>
)}
{[1,2,3].map(i =>
)}
); } const latestStatus = deployments[0]?.status || 'pending'; const hasDeployments = deployments.length > 0; const isStopped = latestStatus === 'stopped'; const isRunning = latestStatus === 'running'; const isInProgress = latestStatus === 'building' || latestStatus === 'deploying' || latestStatus === 'pending'; const handleDelete = () => { if (confirm(`Are you sure you want to delete "${app.name}"?\n\nThis will permanently remove:\n• All Kubernetes resources (pods, services, ingress)\n• Database and volumes\n• All deployment records\n• Uploaded source code`)) { deleteMutation.mutate(); } }; return (
{/* Header */}

{app.name}

{latestStatus}

{app.runtime} · {app.subdomain}.apps.cloudhost.local

{!hasDeployments && ( )} {hasDeployments && ( <> {isStopped ? ( ) : ( )} {isRunning && ( )} {!isInProgress && ( )} {isRunning && ( )} )}
{/* Status & Config */}

Configuration

Runtime
{app.runtime}
Database
{app.databaseType}
Replicas
{app.replicas}
CPU
{app.cpuRequest} / {app.cpuLimit}
Memory
{app.memoryRequest} / {app.memoryLimit}
Port
{app.port}
{app.clusterId && (
Cluster
{clusters.find((c) => c.id === app.clusterId)?.name || 'Unknown'}
)} {app.poolId && (
Pool
{pools.find((p) => p.id === app.poolId)?.name || 'Unknown'}
)} {app.latestImageTag && (
Image
{app.latestImageTag}
)}

Deployment History

{deployments.length === 0 ? (

No deployments yet

Upload source code and click Deploy to get started

) : (
{deployments.slice(0, 10).map((d) => (

{d.version || d.imageTag}

{new Date(d.createdAt).toLocaleString()}

{d.errorMessage && (

{d.errorMessage}

)}
{d.status}
))}
)}
{/* Source Code Upload */}

Source Code

{app.codePath ? (

Source code uploaded

{app.codePath.split('/').pop()}

) : app.gitUrl ? (

Git repository connected

{app.gitUrl}

{app.gitBranch && ( {app.gitBranch} )} {app.gitToken && ( Private )}
) : null}
fileInputRef.current?.click()} className={`border-2 border-dashed rounded-xl p-8 text-center cursor-pointer transition-all ${isDragging ? 'border-primary-500 bg-primary-50' : 'border-gray-300 hover:border-primary-400 hover:bg-gray-50' } ${uploadMutation.isPending ? 'pointer-events-none opacity-60' : ''} `} > { const file = e.target.files?.[0]; if (file) handleFileUpload(file); e.target.value = ''; }} /> {uploadMutation.isPending ? (

Uploading... {uploadProgress}%

) : (

{app.codePath ? 'Upload new version' : 'Upload your project source code'}

Drag & drop a .zip file here, or click to browse

Max size: 100MB

)}
{/* Resource Monitoring & Scaling */}

Resources & Scaling

{showResources && (
{/* Live Metrics */} {resourcesLoading ? (
Loading metrics...
) : resourceUsage ? ( <> {/* Cluster Status */}

Replicas

{resourceUsage.configured.readyReplicas}/{resourceUsage.configured.replicas}

ready

Pods

{resourceUsage.pods.length}

{resourceUsage.pods.filter((p) => p.ready).length} ready

Metrics

{resourceUsage.metrics.length > 0 ? : }

{resourceUsage.metrics.length > 0 ? 'Available' : 'Waiting...'}

{/* Per-Pod Metrics */} {resourceUsage.metrics.length > 0 && (

Pod Usage

{resourceUsage.metrics.map((metric) => { const cpuUsed = parseCpuToMillicores(metric.cpu); const cpuLimit = parseCpuToMillicores(resourceUsage.configured.cpuLimit); const cpuPercent = cpuLimit > 0 ? Math.min((cpuUsed / cpuLimit) * 100, 100) : 0; const memUsed = parseMemoryToMi(metric.memory); const memLimit = parseMemoryToMi(resourceUsage.configured.memoryLimit); const memPercent = memLimit > 0 ? Math.min((memUsed / memLimit) * 100, 100) : 0; return (

{metric.name}

{/* CPU Bar */}
CPU {cpuUsed.toFixed(1)}m / {cpuLimit.toFixed(0)}m ({cpuPercent.toFixed(1)}%)
80 ? 'bg-red-500' : cpuPercent > 50 ? 'bg-yellow-500' : 'bg-green-500' }`} style={{ width: `${cpuPercent}%` }} />
{/* Memory Bar */}
Memory {memUsed.toFixed(1)}Mi / {memLimit.toFixed(0)}Mi ({memPercent.toFixed(1)}%)
80 ? 'bg-red-500' : memPercent > 50 ? 'bg-yellow-500' : 'bg-green-500' }`} style={{ width: `${memPercent}%` }} />
); })}
)} {/* Pod Status Table */} {resourceUsage.pods.length > 0 && (

Pod Status

{resourceUsage.pods.map((pod) => ( ))}
Pod Status Ready Restarts
{pod.name} {pod.status} {pod.ready ? : } {pod.restarts}
)} {/* Scaling Controls */}

Scale Resources

setResourceForm((f) => ({ ...f, cpuRequest: e.target.value }))} className="input-field text-sm" placeholder="100m" />
setResourceForm((f) => ({ ...f, cpuLimit: e.target.value }))} className="input-field text-sm" placeholder="500m" />
setResourceForm((f) => ({ ...f, memoryRequest: e.target.value }))} className="input-field text-sm" placeholder="128Mi" />
setResourceForm((f) => ({ ...f, memoryLimit: e.target.value }))} className="input-field text-sm" placeholder="512Mi" />
{resourceForm.replicas}
) : (

{isStopped ? 'Application is stopped. Start it to see resources.' : 'No resource data available yet.'}

)}
)}
{/* Logs — Pod & Build */}

Logs

{showLogs && logTab === 'pod' && ( Live (every 3s) )} {showLogs && logTab === 'build' && ( Auto-refresh (every 5s) )}
{showLogs && (
{/* Tab switcher */}
{/* Pod logs */} {logTab === 'pod' && (
                {logsData?.logs || (isRunning ? 'Loading logs...' : isStopped ? 'Application is stopped. Start it to see logs.' : 'Waiting for pod to be ready...')}
              
)} {/* Build logs */} {logTab === 'build' && (
{buildLogsData?.version && (
{buildLogsData.version} {buildLogsData.status}
)}
                  {buildLogsData?.buildLog || (
                    buildLogsData?.status === 'building'
                      ? 'Build in progress... Logs will appear when complete.'
                      : buildLogsData?.status === 'pending'
                        ? 'Build is pending...'
                        : buildLogsData?.status === 'no_deployment'
                          ? 'No deployments yet. Deploy your app to see build logs.'
                          : 'No build logs available for this deployment.'
                  )}
                
)}
)}
); }