mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-12 11:47:11 +00:00
fix(dashboard): harden real-time dashboard with bug fixes and design compliance (#517)
* fix(dashboard): harden real-time dashboard with bug fixes and design compliance - Fix host alert spam: add 5-minute cooldown for CPU/RAM/disk threshold alerts, preventing duplicate notifications every 30s during sustained breaches. Extract shared dispatchWithCooldown helper (also used by Docker janitor alerts). - Fix memory metric inflation: subtract filesystem cache from stored memory_mb values, matching the existing calculateMemoryPercent logic. - Fix crash detection reliability: replace fragile 'seconds ago' string matching with a tracked Set of alerted container IDs. Containers are only alerted once per crash event, with automatic cleanup when they start running again or after a 1-hour TTL. - Fix health status bar: exited containers now trigger 'degraded' state independently of unread error notifications. - Fix CPU chart Y-axis: auto-scale when aggregate container CPU exceeds 100% instead of silently clipping at the hardcoded domain ceiling. - Fix grammar: 'actives' to 'active' in container count label. - Add shadow-card-bevel to all dashboard cards per design system. - Update dashboard docs to reflect revised health status thresholds. * test(dashboard): update monitor service tests for new alert signatures - Add container Id fields to crash detection test fixtures - Update host alert assertions to match dispatchWithCooldown 3-arg call - Fix unhealthy container test to use State: 'unhealthy' instead of State: 'running' (running containers are now skipped in crash detect)
This commit is contained in:
@@ -38,13 +38,13 @@ function deriveHealth(stats: Stats, systemStats: SystemStats | null, notificatio
|
||||
if (disk >= 90) reasons.push(`Disk at ${disk.toFixed(1)}%`);
|
||||
else if (disk >= 80) reasons.push(`Disk at ${disk.toFixed(1)}%`);
|
||||
|
||||
if (stats.exited > 0 && unreadErrors > 0) reasons.push(`${stats.exited} exited container${stats.exited !== 1 ? 's' : ''}`);
|
||||
else if (unreadErrors > 0) reasons.push(`${unreadErrors} unread error${unreadErrors !== 1 ? 's' : ''}`);
|
||||
if (stats.exited > 0) reasons.push(`${stats.exited} exited container${stats.exited !== 1 ? 's' : ''}`);
|
||||
if (unreadErrors > 0) reasons.push(`${unreadErrors} unread error${unreadErrors !== 1 ? 's' : ''}`);
|
||||
|
||||
if (cpu >= 90 || ram >= 90 || disk >= 90 || (stats.exited > 0 && unreadErrors > 0)) {
|
||||
return { level: 'critical', reasons };
|
||||
}
|
||||
if (cpu >= 80 || ram >= 80 || disk >= 80 || unreadErrors > 0) {
|
||||
if (cpu >= 80 || ram >= 80 || disk >= 80 || stats.exited > 0 || unreadErrors > 0) {
|
||||
return { level: 'degraded', reasons };
|
||||
}
|
||||
return { level: 'healthy', reasons: ['All systems nominal'] };
|
||||
@@ -65,7 +65,7 @@ export function HealthStatusBar({ stats, systemStats, notifications, activeNodeN
|
||||
const unreadAlerts = notifications.filter(n => !n.is_read).length;
|
||||
|
||||
return (
|
||||
<Card className="bg-card px-4 py-3">
|
||||
<Card className="bg-card shadow-card-bevel px-4 py-3">
|
||||
<div className="flex items-center justify-between gap-4 flex-wrap">
|
||||
{/* Health badge */}
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@@ -45,7 +45,7 @@ export function HistoricalCharts({ metrics, systemStats }: HistoricalChartsProps
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<Card className="bg-card">
|
||||
<Card className="bg-card shadow-card-bevel">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center space-x-2 text-sm font-medium text-stat-title">
|
||||
<Activity className="w-4 h-4 text-stat-icon" strokeWidth={1.5} />
|
||||
@@ -59,7 +59,7 @@ export function HistoricalCharts({ metrics, systemStats }: HistoricalChartsProps
|
||||
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--chart-grid)" />
|
||||
<XAxis dataKey="time" minTickGap={30} tickMargin={8} tick={{ fill: 'var(--chart-tick)', fontSize: 11 }} />
|
||||
<YAxis tickFormatter={(val) => `${Number(val).toFixed(0)}%`} domain={[0, 100]} tick={{ fill: 'var(--chart-tick)', fontSize: 11 }} />
|
||||
<YAxis tickFormatter={(val) => `${Number(val).toFixed(0)}%`} domain={[0, (dataMax: number) => Math.max(100, Math.ceil(dataMax / 10) * 10)]} tick={{ fill: 'var(--chart-tick)', fontSize: 11 }} />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Area type="monotone" dataKey="cpu" stroke="var(--color-cpu)" fill="var(--color-cpu)" fillOpacity={0.4} />
|
||||
</AreaChart>
|
||||
@@ -73,7 +73,7 @@ export function HistoricalCharts({ metrics, systemStats }: HistoricalChartsProps
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card">
|
||||
<Card className="bg-card shadow-card-bevel">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="flex items-center space-x-2 text-sm font-medium text-stat-title">
|
||||
<Activity className="w-4 h-4 text-stat-icon" strokeWidth={1.5} />
|
||||
|
||||
@@ -57,7 +57,7 @@ export function RecentAlerts({ notifications, nodes, onCleared }: RecentAlertsPr
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-card">
|
||||
<Card className="bg-card shadow-card-bevel">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium text-stat-title">Recent Alerts</CardTitle>
|
||||
|
||||
@@ -52,7 +52,7 @@ export function ResourceGauges({ stats, systemStats }: ResourceGaugesProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3">
|
||||
{/* CPU */}
|
||||
<Card className="bg-card">
|
||||
<Card className="bg-card shadow-card-bevel">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
|
||||
<CardTitle className="text-xs font-medium text-stat-title">CPU</CardTitle>
|
||||
<Cpu className="h-3.5 w-3.5 text-stat-icon" strokeWidth={1.5} />
|
||||
@@ -69,7 +69,7 @@ export function ResourceGauges({ stats, systemStats }: ResourceGaugesProps) {
|
||||
</Card>
|
||||
|
||||
{/* RAM */}
|
||||
<Card className="bg-card">
|
||||
<Card className="bg-card shadow-card-bevel">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
|
||||
<CardTitle className="text-xs font-medium text-stat-title">Memory</CardTitle>
|
||||
<MemoryStick className="h-3.5 w-3.5 text-stat-icon" strokeWidth={1.5} />
|
||||
@@ -86,7 +86,7 @@ export function ResourceGauges({ stats, systemStats }: ResourceGaugesProps) {
|
||||
</Card>
|
||||
|
||||
{/* Disk */}
|
||||
<Card className="bg-card">
|
||||
<Card className="bg-card shadow-card-bevel">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
|
||||
<CardTitle className="text-xs font-medium text-stat-title">Disk</CardTitle>
|
||||
<HardDrive className="h-3.5 w-3.5 text-stat-icon" strokeWidth={1.5} />
|
||||
@@ -103,7 +103,7 @@ export function ResourceGauges({ stats, systemStats }: ResourceGaugesProps) {
|
||||
</Card>
|
||||
|
||||
{/* Containers */}
|
||||
<Card className="bg-card">
|
||||
<Card className="bg-card shadow-card-bevel">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
|
||||
<CardTitle className="text-xs font-medium text-stat-title">Containers</CardTitle>
|
||||
<Container className="h-3.5 w-3.5 text-stat-icon" strokeWidth={1.5} />
|
||||
@@ -113,7 +113,7 @@ export function ResourceGauges({ stats, systemStats }: ResourceGaugesProps) {
|
||||
<CursorProvider>
|
||||
<CursorContainer className="inline-flex items-baseline">
|
||||
<span className="text-2xl font-medium font-mono tabular-nums tracking-tight text-stat-value">{stats.active}</span>
|
||||
<span className="text-sm text-stat-subtitle ml-1.5">{stats.active === 1 ? 'active' : 'actives'}</span>
|
||||
<span className="text-sm text-stat-subtitle ml-1.5">active</span>
|
||||
</CursorContainer>
|
||||
<Cursor>
|
||||
<div className="h-2 w-2 rounded-full bg-brand" />
|
||||
@@ -141,7 +141,7 @@ export function ResourceGauges({ stats, systemStats }: ResourceGaugesProps) {
|
||||
</Card>
|
||||
|
||||
{/* Network */}
|
||||
<Card className="bg-card">
|
||||
<Card className="bg-card shadow-card-bevel">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
|
||||
<CardTitle className="text-xs font-medium text-stat-title">Network</CardTitle>
|
||||
<Network className="h-3.5 w-3.5 text-stat-icon" strokeWidth={1.5} />
|
||||
|
||||
@@ -78,7 +78,7 @@ export function StackHealthTable({ stackStatuses, metrics, onNavigateToStack }:
|
||||
|
||||
if (Object.keys(stackStatuses).length === 0) {
|
||||
return (
|
||||
<Card className="bg-card">
|
||||
<Card className="bg-card shadow-card-bevel">
|
||||
<CardContent className="py-8">
|
||||
<div className="flex flex-col items-center justify-center gap-2 text-stat-subtitle">
|
||||
<Layers className="h-8 w-8 text-stat-icon" strokeWidth={1.5} />
|
||||
@@ -90,7 +90,7 @@ export function StackHealthTable({ stackStatuses, metrics, onNavigateToStack }:
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-card">
|
||||
<Card className="bg-card shadow-card-bevel">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-sm font-medium text-stat-title">Stack Health</CardTitle>
|
||||
|
||||
Reference in New Issue
Block a user