Add NUC Portal - infrastructure dashboard

Next.js 16 dashboard for managing NUC services via Coolify API.
Features service cards with health indicators, deployment dashboard
with live log streaming, S3-backed preview images, SSE real-time
updates, and dark mode support. 18 services across 7 categories.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-18 15:17:25 +01:00
parent 8b503a549c
commit 9a0881e852
55 changed files with 15900 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
'use client';
import { usePortal } from '@/lib/PortalContext';
import { getVitalsBg, getVitalsTrack } from '@/lib/stats';
function MiniBar({ label, percent, detail }: { label: string; percent: number; detail?: string }) {
const bg = getVitalsBg(percent);
const track = getVitalsTrack(percent);
return (
<div className="flex items-center gap-1.5">
<span className="text-[11px] font-medium text-slate-500 dark:text-stone-500 w-8 text-right">{label}</span>
<div className={`w-16 h-1.5 rounded-full ${track}`}>
<div
className={`h-full rounded-full ${bg} transition-all duration-500`}
style={{ width: `${Math.min(percent, 100)}%` }}
/>
</div>
<span className="text-[11px] tabular-nums text-slate-600 dark:text-stone-400 w-8">{Math.round(percent)}%</span>
{detail && (
<span className="text-[10px] text-slate-400 dark:text-stone-600 hidden lg:inline">{detail}</span>
)}
</div>
);
}
export function VitalsBar() {
const { systemStats, statsLoading, statsError } = usePortal();
if (statsError && !systemStats) return null;
if (statsLoading && !systemStats) {
return (
<div className="hidden sm:flex items-center gap-4 px-4 sm:px-6 py-1 border-t border-slate-100 dark:border-stone-800/50">
{[1, 2, 3].map(i => (
<div key={i} className="flex items-center gap-1.5">
<div className="w-8 h-2 rounded bg-slate-200 dark:bg-stone-800 animate-pulse" />
<div className="w-16 h-1.5 rounded-full bg-slate-200 dark:bg-stone-800 animate-pulse" />
<div className="w-8 h-2 rounded bg-slate-200 dark:bg-stone-800 animate-pulse" />
</div>
))}
</div>
);
}
if (!systemStats) return null;
const ramDetail = `${(systemStats.ram_used_mb / 1024).toFixed(1)}/${(systemStats.ram_total_mb / 1024).toFixed(1)}G`;
const showSwap = systemStats.swap_percent > 50;
return (
<div className="hidden sm:flex items-center gap-4 px-4 sm:px-6 py-1 border-t border-slate-100 dark:border-stone-800/50">
<MiniBar label="CPU" percent={systemStats.cpu_percent} />
<MiniBar label="RAM" percent={systemStats.ram_percent} detail={ramDetail} />
<MiniBar label="Disk" percent={systemStats.disk_percent} />
{showSwap && <MiniBar label="Swap" percent={systemStats.swap_percent} />}
</div>
);
}