Show running services as named chips with restart/stop on Overview

Replace anonymous green dots with named green chips, each with
restart and stop icon buttons. Now all services have visible
controls on the Overview regardless of state.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-04 00:43:16 +01:00
parent eda6d956b2
commit d4053812cd

View File

@@ -2,6 +2,7 @@
import { useState, useCallback } from 'react';
import { usePortal } from '@/lib/PortalContext';
import { clientConfig } from '@/lib/config';
import { Icon } from './Icons';
import { SystemTrends } from './SystemTrends';
import { formatUptime } from '@/lib/stats';
@@ -14,15 +15,6 @@ function isDiscoveredService(s: Service): s is DiscoveredService {
return 'uuid' in s && 'resourceType' in s;
}
const quickLinks = [
{ name: 'Coolify', url: 'http://192.168.1.3:8000', icon: 'coolify', desc: 'Service manager' },
{ name: 'Dozzle', url: 'http://192.168.1.3:9999', icon: 'scroll-text', desc: 'Container logs' },
{ name: 'Uptime Kuma', url: 'http://192.168.1.3:3001', icon: 'activity', desc: 'Monitoring' },
{ name: 'Ntfy', url: 'http://192.168.1.3:8333', icon: 'bell', desc: 'Notifications' },
{ name: 'Gitea', url: 'http://192.168.1.3:3030', icon: 'git-branch', desc: 'Git hosting' },
{ name: 'Adminer', url: 'http://192.168.1.3:8088', icon: 'database', desc: 'DB admin' },
];
interface ProjectDef {
name: string;
icon: string;
@@ -135,7 +127,6 @@ export function OverviewTab() {
body: JSON.stringify({ uuid: s.uuid, resourceType: s.resourceType, action }),
});
if (res.ok) {
// Wait briefly for Coolify to process, then refresh
setTimeout(() => refreshDiscover(), 3000);
}
} catch { /* ignore */ } finally {
@@ -143,6 +134,26 @@ export function OverviewTab() {
}
}, [refreshDiscover]);
// Build quick links dynamically from discovered services
const quickLinkDefs = [
{ key: 'coolify', icon: 'coolify', desc: 'Service manager' },
{ key: 'dozzle', icon: 'scroll-text', desc: 'Container logs' },
{ key: 'uptime kuma', icon: 'activity', desc: 'Monitoring' },
{ key: 'ntfy', icon: 'bell', desc: 'Notifications' },
{ key: 'gitea', icon: 'git-branch', desc: 'Git hosting' },
{ key: 'adminer', icon: 'database', desc: 'DB admin' },
];
const quickLinks = quickLinkDefs.map(def => {
const svc = services.find(s => s.name.toLowerCase().includes(def.key));
return {
name: svc?.name || def.key.charAt(0).toUpperCase() + def.key.slice(1),
url: svc?.url || `http://${clientConfig.nucHost}`,
icon: svc?.icon || def.icon,
desc: def.desc,
};
});
const runningServices = services.filter(s => healthStatus[s.name] === 'running');
const stoppedServices = services.filter(s => healthStatus[s.name] === 'stopped');
const unknownServices = services.filter(s => {
@@ -184,7 +195,6 @@ export function OverviewTab() {
<span className="text-sm text-slate-500 dark:text-stone-500">of {totalCount} running</span>
</div>
{/* Stopped services as named chips with start button */}
{stoppedServices.length > 0 && (
<div className="mb-3">
<p className="text-[11px] font-medium text-red-500/70 dark:text-red-400/70 uppercase tracking-wider mb-1.5">Stopped</p>
@@ -220,7 +230,6 @@ export function OverviewTab() {
</div>
)}
{/* Unknown services as named chips with start button */}
{unknownServices.length > 0 && (
<div className="mb-3">
<p className="text-[11px] font-medium text-slate-400/70 dark:text-stone-500/70 uppercase tracking-wider mb-1.5">Unknown</p>
@@ -256,16 +265,48 @@ export function OverviewTab() {
</div>
)}
{/* Running services as green dots */}
{runningServices.length > 0 && (
<div className="flex flex-wrap gap-1">
{runningServices.map(s => (
<div
key={s.name}
title={s.name}
className="w-2.5 h-2.5 rounded-sm bg-emerald-500"
/>
))}
<div>
<p className="text-[11px] font-medium text-emerald-500/70 dark:text-emerald-400/70 uppercase tracking-wider mb-1.5">Running</p>
<div className="flex flex-wrap gap-1.5">
{runningServices.map(s => {
const uuid = isDiscoveredService(s) ? s.uuid : null;
const loading = uuid ? controlling[uuid] : false;
return (
<span
key={s.name}
className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 border border-emerald-100 dark:border-emerald-800/30"
>
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 flex-shrink-0" />
{s.name}
{uuid && (
<span className="flex items-center gap-0.5 ml-0.5">
<button
onClick={() => controlService(s, 'restart')}
disabled={loading}
title={`Restart ${s.name}`}
className="p-0.5 rounded hover:bg-emerald-100 dark:hover:bg-emerald-800/30 transition-colors disabled:opacity-50"
>
{loading ? (
<Icon name="loader" size={11} className="animate-spin" />
) : (
<Icon name="refresh-cw" size={11} />
)}
</button>
<button
onClick={() => controlService(s, 'stop')}
disabled={loading}
title={`Stop ${s.name}`}
className="p-0.5 rounded hover:bg-red-100 dark:hover:bg-red-800/30 hover:text-red-500 transition-colors disabled:opacity-50"
>
<Icon name="power" size={11} />
</button>
</span>
)}
</span>
);
})}
</div>
</div>
)}
</div>
@@ -317,7 +358,7 @@ export function OverviewTab() {
</div>
</div>
{/* Row 3: Projects (side by side, scales with more projects) */}
{/* Row 3: Projects */}
<div>
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
<Icon name="box" size={16} className="text-slate-400 dark:text-stone-500" />