Add WhyOps service and show health status for all services

- Add WhyOps (whyops.nuc.lan:3002) to service catalog and registry
- Show status pill for static (non-Coolify) services too
- Merge static services into discovered list so they always appear
- Health-check static services via /api/health endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-08 18:54:54 +00:00
parent 331cd621cf
commit 16d81c4ec3
4 changed files with 40 additions and 7 deletions

View File

@@ -144,7 +144,6 @@ export function ServiceCard({ service, status }: ServiceCardProps) {
{/* Footer: status + links + controls */}
<div className="mt-3 pt-3 border-t border-slate-100 dark:border-stone-800 flex items-center justify-between">
{/* Left: status pill */}
{discovered ? (
<span className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[11px] font-medium ${statusPillStyles[status]}`}>
<Icon
name={loading ? 'loader' : statusIcons[status]}
@@ -153,9 +152,6 @@ export function ServiceCard({ service, status }: ServiceCardProps) {
/>
{loading ? 'Processing...' : statusLabels[status]}
</span>
) : (
<span />
)}
{/* Right: links + action buttons */}
<div className="flex items-center gap-1">

View File

@@ -93,9 +93,43 @@ export function PortalProvider({ children }: { children: ReactNode }) {
}
}
// Active services: discovered or fallback
// Health-check static (non-discovered) services via /api/health
const [staticHealth, setStaticHealth] = useState<HealthState>({});
useEffect(() => {
if (discoveredServices.length === 0) return;
const statics = fallbackServices.filter(fb =>
!discoveredServices.some(d => d.name === fb.name || d.port === fb.port)
);
if (statics.length === 0) return;
let cancelled = false;
async function check() {
try {
const res = await fetch('/api/health');
if (!res.ok) return;
const data = await res.json();
if (!cancelled) setStaticHealth(data);
} catch { /* ignore */ }
}
check();
const interval = setInterval(check, 30000);
return () => { cancelled = true; clearInterval(interval); };
}, [discoveredServices.length]);
// Merge static health into healthStatus
for (const [name, status] of Object.entries(staticHealth)) {
if (!healthStatus[name]) {
healthStatus[name] = status;
}
}
// Active services: discovered + any fallback services not already discovered
const activeServices: Service[] = discoveredServices.length > 0
? discoveredServices
? [
...discoveredServices,
...fallbackServices.filter(fb =>
!discoveredServices.some(d => d.name === fb.name || d.port === fb.port)
),
]
: fallbackServices;
// Filter services and bookmarks

View File

@@ -66,6 +66,8 @@ const registry: Record<string, ServiceMeta> = {
// Apps
'googlescraper': { icon: 'search', category: 'automation', description: 'Google scraper API' },
'whyops': { icon: 'settings', category: 'automation', description: 'WhyRating scraping, pipelines & testing' },
'whyrating-dashboard': { icon: 'settings', category: 'automation', description: 'WhyRating scraping, pipelines & testing' },
'actionkit landing': { icon: 'layout', category: 'development', description: 'Landing page builder' },
};

View File

@@ -60,6 +60,7 @@ export const fallbackServices: Service[] = [
{ name: 'Gitea', url: 'http://gitea.nuc.lan', port: 3030, icon: 'git-branch', category: 'development', description: 'Self-hosted Git service' },
{ name: 'CloudBeaver', url: `http://${h}:8978`, port: 8978, icon: 'database', category: 'development', description: 'Database management UI' },
{ name: 'Adminer', url: `http://${h}:8088`, port: 8088, icon: 'table', category: 'development', description: 'Lightweight database admin' },
{ name: 'WhyOps', url: 'http://whyops.nuc.lan', port: 3002, icon: 'settings', category: 'automation', description: 'WhyRating scraping, pipelines & testing' },
// Knowledge - prefer domain-based URLs
{ name: 'Outline', url: 'http://outline.nuc.lan', port: 3080, icon: 'book-open', category: 'knowledge', description: 'Team wiki & documentation' },