From db7b3852873284650a90d30b6b79961f5c5a4304 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 21 Dec 2025 11:24:42 +0000 Subject: [PATCH] feat(ui): wire memory and disk anomaly indicators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the anomaly indicator integration for all three metrics: - CPU: EnhancedCPUBar (already done) - Memory: StackedMemoryBar (new) - Disk: StackedDiskBar (new) All three metric bars now show a pulsing indicator (e.g., '2.5x↑') when the current value is significantly above the learned baseline. Severity colors: - Critical (>4σ): red - High (3-4σ): orange - Medium (2.5-3σ): yellow - Low (2-2.5σ): blue This is 100% deterministic - no LLM involved. The indicators appear automatically based on statistical deviation from learned baselines. --- .../src/components/Dashboard/GuestRow.tsx | 6 ++-- .../components/Dashboard/StackedDiskBar.tsx | 29 +++++++++++++++++++ .../components/Dashboard/StackedMemoryBar.tsx | 28 ++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/frontend-modern/src/components/Dashboard/GuestRow.tsx b/frontend-modern/src/components/Dashboard/GuestRow.tsx index 3096e6c..1a52954 100644 --- a/frontend-modern/src/components/Dashboard/GuestRow.tsx +++ b/frontend-modern/src/components/Dashboard/GuestRow.tsx @@ -522,9 +522,7 @@ export function GuestRow(props: GuestRowProps) { const cpuAnomaly = useAnomalyForMetric(() => props.guest.id, () => 'cpu'); const memoryAnomaly = useAnomalyForMetric(() => props.guest.id, () => 'memory'); const diskAnomaly = useAnomalyForMetric(() => props.guest.id, () => 'disk'); - // TODO: Wire memoryAnomaly and diskAnomaly to StackedMemoryBar and StackedDiskBar - void memoryAnomaly; // Prepared for future use - void diskAnomaly; // Prepared for future use + const [customUrl, setCustomUrl] = createSignal(props.customUrl); const [shouldAnimateIcon, setShouldAnimateIcon] = createSignal(false); @@ -960,6 +958,7 @@ export function GuestRow(props: GuestRowProps) { swapUsed={props.guest.memory?.swapUsed || 0} swapTotal={props.guest.memory?.swapTotal || 0} resourceId={metricsKey()} + anomaly={memoryAnomaly()} /> } > @@ -1002,6 +1001,7 @@ export function GuestRow(props: GuestRowProps) { } > diff --git a/frontend-modern/src/components/Dashboard/StackedDiskBar.tsx b/frontend-modern/src/components/Dashboard/StackedDiskBar.tsx index 5b15d06..158b311 100644 --- a/frontend-modern/src/components/Dashboard/StackedDiskBar.tsx +++ b/frontend-modern/src/components/Dashboard/StackedDiskBar.tsx @@ -2,14 +2,25 @@ import { Show, For, createMemo, createSignal, onMount, onCleanup } from 'solid-j import { Portal } from 'solid-js/web'; import type { Disk } from '@/types/api'; import { formatBytes, formatPercent } from '@/utils/format'; +import type { AnomalyReport } from '@/types/aiIntelligence'; interface StackedDiskBarProps { /** Array of disk objects - if empty/undefined, falls back to aggregate */ disks?: Disk[]; /** Aggregate disk data (fallback when disks array unavailable) */ aggregateDisk?: Disk; + /** Baseline anomaly if detected */ + anomaly?: AnomalyReport | null; } +// Anomaly severity colors +const anomalySeverityClass: Record = { + critical: 'text-red-400', + high: 'text-orange-400', + medium: 'text-yellow-400', + low: 'text-blue-400', +}; + // Color palette for disk segments - distinct colors for visual differentiation const SEGMENT_COLORS = [ 'rgba(34, 197, 94, 0.6)', // green @@ -52,6 +63,15 @@ export function StackedDiskBar(props: StackedDiskBarProps) { onCleanup(() => observer.disconnect()); }); + // Format anomaly ratio for display + const anomalyRatio = createMemo(() => { + if (!props.anomaly || props.anomaly.baseline_mean === 0) return null; + const ratio = props.anomaly.current_value / props.anomaly.baseline_mean; + if (ratio >= 2) return `${ratio.toFixed(1)}x`; + if (ratio >= 1.5) return '↑↑'; + return '↑'; + }); + // Determine if we have multiple disks or should use aggregate const hasMultipleDisks = createMemo(() => { const disks = props.disks; @@ -239,6 +259,15 @@ export function StackedDiskBar(props: StackedDiskBarProps) { [{props.disks?.length}] + {/* Anomaly indicator */} + + + {anomalyRatio()} + + diff --git a/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx b/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx index 3da92b8..63c375f 100644 --- a/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx +++ b/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx @@ -4,6 +4,7 @@ import { formatBytes, formatPercent } from '@/utils/format'; import { Sparkline } from '@/components/shared/Sparkline'; import { useMetricsViewMode } from '@/stores/metricsViewMode'; import { getMetricHistoryForRange, getMetricsVersion } from '@/stores/metricsHistory'; +import type { AnomalyReport } from '@/types/aiIntelligence'; interface StackedMemoryBarProps { used: number; @@ -12,8 +13,17 @@ interface StackedMemoryBarProps { swapTotal?: number; balloon?: number; resourceId?: string; // Required for sparkline mode to fetch history + anomaly?: AnomalyReport | null; // Baseline anomaly if detected } +// Anomaly severity colors +const anomalySeverityClass: Record = { + critical: 'text-red-400', + high: 'text-orange-400', + medium: 'text-yellow-400', + low: 'text-blue-400', +}; + // Colors for memory segments const MEMORY_COLORS = { active: 'rgba(34, 197, 94, 0.6)', // green (base, overridden by threshold) @@ -40,6 +50,15 @@ export function StackedMemoryBar(props: StackedMemoryBarProps) { return getMetricHistoryForRange(props.resourceId, timeRange()); }); + // Format anomaly ratio for display + const anomalyRatio = createMemo(() => { + if (!props.anomaly || props.anomaly.baseline_mean === 0) return null; + const ratio = props.anomaly.current_value / props.anomaly.baseline_mean; + if (ratio >= 2) return `${ratio.toFixed(1)}x`; + if (ratio >= 1.5) return '↑↑'; + return '↑'; + }); + const [containerWidth, setContainerWidth] = createSignal(100); const [showTooltip, setShowTooltip] = createSignal(false); const [tooltipPos, setTooltipPos] = createSignal({ x: 0, y: 0 }); @@ -195,6 +214,15 @@ export function StackedMemoryBar(props: StackedMemoryBarProps) { ({displaySublabel()}) + {/* Anomaly indicator */} + + + {anomalyRatio()} + +