From 14fc60875423b7074575c78cc15915152403e042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:58:17 +0100 Subject: [PATCH] Add restart/stop controls to every ServiceCard on Services tab Running services show Restart and Stop buttons. Stopped/unknown show Start button. Stop requires double-click confirmation (3s timeout). Card restructured from wrapper to
with nested for the link area, so buttons can coexist without nesting issues. Co-Authored-By: Claude Opus 4.5 --- src/components/ServiceCard.tsx | 158 +++++++++++++++++++++++++-------- 1 file changed, 120 insertions(+), 38 deletions(-) diff --git a/src/components/ServiceCard.tsx b/src/components/ServiceCard.tsx index da8c7f5..a5ae3d1 100644 --- a/src/components/ServiceCard.tsx +++ b/src/components/ServiceCard.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState, useCallback } from 'react'; import { Service, DiscoveredService } from '@/lib/services'; import { HealthStatus } from '@/lib/PortalContext'; import { Icon } from './Icons'; @@ -32,7 +33,6 @@ function getFqdnLabel(service: Service): string | null { try { const url = new URL(service.fqdn); const hostname = url.hostname; - // Show just the subdomain part if it's a .nuc.lan address if (hostname.endsWith('.nuc.lan')) { return hostname.replace('.nuc.lan', ''); } @@ -52,14 +52,41 @@ function getResourceBadge(service: Service): string | 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 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, + }), + }); + // Coolify takes a moment to process — page will refresh via polling + } 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 ( - +
{/* Status indicator */}
{resourceBadge && ( @@ -73,38 +100,93 @@ export function ServiceCard({ service, status }: ServiceCardProps) { />
- {/* Icon */} -
- -
+ {/* Clickable area for opening service URL */} +
+ {/* Icon */} +
+ +
- {/* Content */} -

- {service.name} -

- {service.description && ( -

- {service.description} -

+ {/* Content */} +

+ {service.name} +

+ {service.description && ( +

+ {service.description} +

+ )} + + {/* Badge: FQDN subdomain or port */} +
+ {fqdnLabel && ( + + {fqdnLabel} + + )} + {service.port > 0 && ( + + :{service.port} + + )} +
+
+ + {/* Control buttons for discovered services */} + {discovered && ( +
+ {status === 'stopped' || status === 'unknown' ? ( + + ) : status === 'running' ? ( + <> + + + + ) : null} +
)} - - {/* Badge: FQDN subdomain or port */} -
- {fqdnLabel && ( - - {fqdnLabel} - - )} - {service.port > 0 && ( - - :{service.port} - - )} -
- +
); }