Add auto-discovery services tab using Coolify API

Replace static services list with dynamic discovery from Coolify's
server resources API. Services are auto-categorized using a registry
of known service names mapped to icons and categories. Falls back to
static list with health checks when Coolify is unreachable.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-03 21:28:02 +01:00
parent 3a16df2581
commit d70f7a902f
6 changed files with 526 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { Service } from '@/lib/services';
import { Service, DiscoveredService } from '@/lib/services';
import { HealthStatus } from '@/lib/PortalContext';
import { Icon } from './Icons';
@@ -23,7 +23,36 @@ const statusLabels: Record<HealthStatus, string> = {
loading: 'Checking...',
};
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;
// Show just the subdomain part if it's a .nuc.lan address
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);
return (
<a
href={service.url}
@@ -33,6 +62,11 @@ export function ServiceCard({ service, status }: ServiceCardProps) {
>
{/* Status indicator */}
<div className="absolute top-3 right-3 flex items-center gap-1.5">
{resourceBadge && (
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-slate-100 dark:bg-stone-800 text-slate-500 dark:text-stone-400">
{resourceBadge}
</span>
)}
<span
className={`w-2 h-2 rounded-full ${statusColors[status]}`}
title={statusLabels[status]}
@@ -58,9 +92,18 @@ export function ServiceCard({ service, status }: ServiceCardProps) {
</p>
)}
{/* Port badge */}
<div className="mt-3 text-xs text-slate-400 dark:text-stone-600 font-mono">
:{service.port}
{/* Badge: FQDN subdomain or port */}
<div className="mt-3 flex items-center gap-2">
{fqdnLabel && (
<span className="text-xs px-1.5 py-0.5 rounded bg-cyan-50 dark:bg-cyan-900/20 text-cyan-600 dark:text-cyan-400 font-mono">
{fqdnLabel}
</span>
)}
{service.port > 0 && (
<span className="text-xs text-slate-400 dark:text-stone-600 font-mono">
:{service.port}
</span>
)}
</div>
</a>
);