Reorder Overview layout: charts hero, remove redundant health card

- System Trends promoted to full-width hero row (most important info first)
- Uptime and load average moved into Trends card header
- Removed separate System Health card (redundant with VitalsBar + Trends)
- Services + Deployments in row 2, Quick Links + WhyRating in row 3
- Taller charts (h-20), tighter gaps, cleaner visual hierarchy

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-03 22:47:50 +01:00
parent 43936fc601
commit 2e0bf435fe
2 changed files with 49 additions and 117 deletions

View File

@@ -3,37 +3,10 @@
import { usePortal } from '@/lib/PortalContext';
import { Icon } from './Icons';
import { SystemTrends } from './SystemTrends';
import { getVitalsBg, getVitalsTrack, formatUptime } from '@/lib/stats';
import { formatUptime } from '@/lib/stats';
import { STATUS_COLORS, STATUS_LABELS, formatRelativeTime, formatDuration } from '@/lib/deployments';
import type { DeploymentStatus } from '@/lib/deployments';
function StatBar({ label, percent, used, total, unit }: {
label: string;
percent: number;
used: string;
total: string;
unit: string;
}) {
const bg = getVitalsBg(percent);
const track = getVitalsTrack(percent);
return (
<div className="space-y-1">
<div className="flex justify-between text-sm">
<span className="text-slate-600 dark:text-stone-400">{label}</span>
<span className="text-slate-900 dark:text-stone-100 tabular-nums font-medium">{Math.round(percent)}%</span>
</div>
<div className={`w-full h-2 rounded-full ${track}`}>
<div
className={`h-full rounded-full ${bg} transition-all duration-500`}
style={{ width: `${Math.min(percent, 100)}%` }}
/>
</div>
<p className="text-xs text-slate-400 dark:text-stone-600 tabular-nums">{used} / {total} {unit}</p>
</div>
);
}
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' },
@@ -52,7 +25,6 @@ const whyratingApps = [
export function OverviewTab() {
const {
systemStats,
statsLoading,
services,
healthStatus,
deployments,
@@ -68,69 +40,17 @@ export function OverviewTab() {
const isDiscovered = discoveredServices.length > 0;
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 max-w-6xl">
{/* System Health */}
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-4 flex items-center gap-2">
<Icon name="activity" size={16} className="text-slate-400 dark:text-stone-500" />
System Health
</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5 max-w-6xl">
{statsLoading && !systemStats ? (
<div className="space-y-4">
{[1, 2, 3].map(i => (
<div key={i} className="space-y-1">
<div className="h-4 w-24 rounded bg-slate-200 dark:bg-stone-800 animate-pulse" />
<div className="h-2 w-full rounded-full bg-slate-200 dark:bg-stone-800 animate-pulse" />
<div className="h-3 w-20 rounded bg-slate-200 dark:bg-stone-800 animate-pulse" />
</div>
))}
</div>
) : systemStats ? (
<div className="space-y-4">
<StatBar
label="CPU"
percent={systemStats.cpu_percent}
used={`${systemStats.cpu_percent}%`}
total="100%"
unit=""
/>
<StatBar
label="RAM"
percent={systemStats.ram_percent}
used={`${(systemStats.ram_used_mb / 1024).toFixed(1)}G`}
total={`${(systemStats.ram_total_mb / 1024).toFixed(1)}G`}
unit=""
/>
<StatBar
label="Disk"
percent={systemStats.disk_percent}
used={`${systemStats.disk_used_gb}G`}
total={`${systemStats.disk_total_gb}G`}
unit=""
/>
{systemStats.swap_percent > 50 && (
<StatBar
label="Swap"
percent={systemStats.swap_percent}
used={`${(systemStats.swap_used_mb / 1024).toFixed(1)}G`}
total={`${(systemStats.swap_total_mb / 1024).toFixed(1)}G`}
unit=""
/>
)}
<div className="flex items-center gap-4 pt-2 text-xs text-slate-500 dark:text-stone-500 border-t border-slate-100 dark:border-stone-800">
<span>Uptime: {formatUptime(systemStats.uptime_seconds)}</span>
<span>Load: {systemStats.load_avg.join(' / ')}</span>
</div>
</div>
) : (
<p className="text-sm text-slate-400 dark:text-stone-600">Stats unavailable</p>
)}
</div>
{/* Row 1: System Trends (full-width hero) with live stats in header */}
<SystemTrends
uptimeLabel={systemStats ? formatUptime(systemStats.uptime_seconds) : undefined}
loadAvg={systemStats?.load_avg}
/>
{/* Service Summary */}
{/* Row 2 left: Services */}
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-4 flex items-center gap-2">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
<Icon name="server" size={16} className="text-slate-400 dark:text-stone-500" />
Services
{isDiscovered && (
@@ -140,12 +60,12 @@ export function OverviewTab() {
)}
</h2>
<div className="flex items-baseline gap-3 mb-4">
<div className="flex items-baseline gap-3 mb-3">
<span className="text-3xl font-bold text-slate-900 dark:text-stone-100 tabular-nums">{runningCount}</span>
<span className="text-sm text-slate-500 dark:text-stone-500">of {totalCount} running</span>
</div>
<div className="flex gap-3 mb-4">
<div className="flex gap-3 mb-3">
<div className="flex items-center gap-1.5 text-sm">
<span className="w-2 h-2 rounded-full bg-emerald-500" />
<span className="text-slate-600 dark:text-stone-400">{runningCount} running</span>
@@ -164,7 +84,6 @@ export function OverviewTab() {
)}
</div>
{/* Mini service status dots */}
<div className="flex flex-wrap gap-1">
{services.map(s => {
const status = healthStatus[s.name];
@@ -180,12 +99,9 @@ export function OverviewTab() {
</div>
</div>
{/* System Trends (6h from Prometheus) */}
<SystemTrends />
{/* Recent Deployments */}
{/* Row 2 right: Recent Deployments */}
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-4 flex items-center gap-2">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
<Icon name="rocket" size={16} className="text-slate-400 dark:text-stone-500" />
Recent Deployments
</h2>
@@ -227,9 +143,9 @@ export function OverviewTab() {
)}
</div>
{/* Quick Links */}
{/* Row 3 left: Quick Links */}
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-4 flex items-center gap-2">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
<Icon name="external-link" size={16} className="text-slate-400 dark:text-stone-500" />
Quick Links
</h2>
@@ -253,14 +169,14 @@ export function OverviewTab() {
</div>
</div>
{/* WhyRating Project */}
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5 lg:col-span-2">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-4 flex items-center gap-2">
{/* Row 3 right: WhyRating Project */}
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 mb-3 flex items-center gap-2">
<Icon name="whyrating" size={16} />
WhyRating Project
</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="space-y-2">
{whyratingApps.map(app => {
const svc = services.find(s => s.url === app.url);
const status = svc ? healthStatus[svc.name] : undefined;

View File

@@ -30,7 +30,7 @@ function useMetrics() {
useEffect(() => {
refresh();
const interval = setInterval(refresh, 60000); // refresh every 60s
const interval = setInterval(refresh, 60000);
return () => clearInterval(interval);
}, [refresh]);
@@ -58,13 +58,13 @@ function SparkChart({ label, series, color, fillColor, formatValue, domain, unit
return (
<div>
<div className="flex items-baseline justify-between mb-1">
<div className="flex items-baseline justify-between mb-1.5">
<span className="text-xs font-medium text-slate-500 dark:text-stone-500">{label}</span>
<span className="text-xs font-semibold tabular-nums" style={{ color }}>
<span className="text-sm font-semibold tabular-nums" style={{ color }}>
{formatValue(lastVal)}{unit || ''}
</span>
</div>
<div className="h-16">
<div className="h-20">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 2, right: 0, bottom: 0, left: 0 }}>
<defs>
@@ -116,28 +116,43 @@ function SparkChart({ label, series, color, fillColor, formatValue, domain, unit
function ShimmerChart() {
return (
<div>
<div className="flex items-baseline justify-between mb-1">
<div className="flex items-baseline justify-between mb-1.5">
<div className="h-3 w-10 rounded bg-slate-200 dark:bg-stone-800 animate-pulse" />
<div className="h-3 w-12 rounded bg-slate-200 dark:bg-stone-800 animate-pulse" />
</div>
<div className="h-16 rounded bg-slate-100 dark:bg-stone-800/50 animate-pulse" />
<div className="h-20 rounded bg-slate-100 dark:bg-stone-800/50 animate-pulse" />
</div>
);
}
export function SystemTrends() {
interface SystemTrendsProps {
uptimeLabel?: string;
loadAvg?: [number, number, number];
}
export function SystemTrends({ uptimeLabel, loadAvg }: SystemTrendsProps) {
const { data, loading, error } = useMetrics();
if (error && !data) return null;
return (
<div className="bg-white dark:bg-stone-900 rounded-xl border border-slate-100 dark:border-stone-700/50 shadow-sm p-5 lg:col-span-2">
{/* Header with title, live stats, and Grafana link */}
<div className="flex items-center justify-between mb-4">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 flex items-center gap-2">
<Icon name="activity" size={16} className="text-slate-400 dark:text-stone-500" />
System Trends
<span className="text-[10px] font-normal text-slate-400 dark:text-stone-600">6h</span>
</h2>
<div className="flex items-center gap-3">
<h2 className="text-sm font-semibold text-slate-900 dark:text-stone-100 flex items-center gap-2">
<Icon name="activity" size={16} className="text-slate-400 dark:text-stone-500" />
System Trends
<span className="text-[10px] font-normal text-slate-400 dark:text-stone-600">6h</span>
</h2>
{(uptimeLabel || loadAvg) && (
<div className="hidden sm:flex items-center gap-3 text-[11px] text-slate-400 dark:text-stone-600">
<span className="w-px h-3 bg-slate-200 dark:bg-stone-700" />
{uptimeLabel && <span>Up {uptimeLabel}</span>}
{loadAvg && <span>Load {loadAvg.map(v => v.toFixed(1)).join(' ')}</span>}
</div>
)}
</div>
<a
href="http://192.168.1.3:3333"
target="_blank"
@@ -149,14 +164,15 @@ export function SystemTrends() {
</a>
</div>
{/* Charts */}
{loading && !data ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
<ShimmerChart />
<ShimmerChart />
<ShimmerChart />
</div>
) : data ? (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
<SparkChart
label="CPU"
series={data.cpu}