'use client'; import { useState, useCallback } from 'react'; import { Service, DiscoveredService, getCoolifyUrl, getDozzleUrl } from '@/lib/services'; import { HealthStatus } from '@/lib/PortalContext'; import { Icon } from './Icons'; interface ServiceCardProps { service: Service; status: HealthStatus; } const borderColors: Record = { running: 'border-l-emerald-500', stopped: 'border-l-red-500', unknown: 'border-l-slate-400 dark:border-l-stone-600', loading: 'border-l-amber-500', }; const statusPillStyles: Record = { running: 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400', stopped: 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400', unknown: 'bg-slate-100 dark:bg-stone-800 text-slate-500 dark:text-stone-500', loading: 'bg-amber-50 dark:bg-amber-900/20 text-amber-600 dark:text-amber-400', }; const statusLabels: Record = { running: 'Running', stopped: 'Stopped', unknown: 'Unknown', loading: 'Checking...', }; const statusIcons: Record = { running: 'circle', stopped: 'power', unknown: 'circle', loading: 'loader', }; function isDiscovered(service: Service): service is DiscoveredService { return 'source' in service && (service as DiscoveredService).source === 'discovered'; } function getFqdnLabel(service: Service): string | null { if (!isDiscovered(service) || !service.fqdn) return null; try { const url = new URL(service.fqdn); const hostname = url.hostname; if (hostname.endsWith('.nuc.lan')) { return hostname.replace('.nuc.lan', ''); } return hostname; } catch { return null; } } function getResourceBadge(service: Service): string | null { if (!isDiscovered(service)) return null; if (service.resourceType === 'database') return 'DB'; if (service.resourceType === 'application') return 'App'; return null; } export function ServiceCard({ service, status }: ServiceCardProps) { const fqdnLabel = getFqdnLabel(service); const resourceBadge = getResourceBadge(service); const discovered = isDiscovered(service); const [loading, setLoading] = useState(false); const [confirmStop, setConfirmStop] = useState(false); const isStopped = status === 'stopped' || status === 'unknown'; const controlService = useCallback(async (action: 'start' | 'stop' | 'restart') => { if (!discovered) return; setLoading(true); setConfirmStop(false); try { await fetch('/api/control', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uuid: service.uuid, resourceType: service.resourceType, action, }), }); } catch { /* ignore */ } finally { setTimeout(() => setLoading(false), 3000); } }, [discovered, service]); const handleStop = useCallback(() => { if (confirmStop) { controlService('stop'); } else { setConfirmStop(true); setTimeout(() => setConfirmStop(false), 3000); } }, [confirmStop, controlService]); return (
{/* Top row: badge */}
{resourceBadge && ( {resourceBadge} )}
{/* Service info (non-clickable) */}

{service.name}

{service.description && (

{service.description}

)}
{fqdnLabel && ( {fqdnLabel} )} {service.port > 0 && ( :{service.port} )}
{/* Footer: status + links + controls */}
{/* Left: status pill */} {discovered ? ( {loading ? 'Processing...' : statusLabels[status]} ) : ( )} {/* Right: links + action buttons */}
{/* Open website */} {/* View logs in Dozzle */} {discovered && ( )} {/* Manage in Coolify */} {discovered && ( )} {/* Divider between links and controls */} {discovered && !loading && (status === 'running' || isStopped) && ( )} {/* Control buttons */} {discovered && !loading && ( <> {isStopped ? ( ) : status === 'running' ? ( <> ) : null} )}
); }