feat(ui): wire memory and disk anomaly indicators
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.
This commit is contained in:
parent
9aad21169d
commit
db7b385287
3 changed files with 60 additions and 3 deletions
|
|
@ -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<string | undefined>(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) {
|
|||
<StackedDiskBar
|
||||
disks={props.guest.disks}
|
||||
aggregateDisk={props.guest.disk}
|
||||
anomaly={diskAnomaly()}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
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}]
|
||||
</span>
|
||||
</Show>
|
||||
{/* Anomaly indicator */}
|
||||
<Show when={props.anomaly && anomalyRatio()}>
|
||||
<span
|
||||
class={`ml-0.5 font-bold animate-pulse ${anomalySeverityClass[props.anomaly!.severity] || 'text-yellow-400'}`}
|
||||
title={props.anomaly!.description}
|
||||
>
|
||||
{anomalyRatio()}
|
||||
</span>
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
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()})
|
||||
</span>
|
||||
</Show>
|
||||
{/* Anomaly indicator */}
|
||||
<Show when={props.anomaly && anomalyRatio()}>
|
||||
<span
|
||||
class={`ml-0.5 font-bold animate-pulse ${anomalySeverityClass[props.anomaly!.severity] || 'text-yellow-400'}`}
|
||||
title={props.anomaly!.description}
|
||||
>
|
||||
{anomalyRatio()}
|
||||
</span>
|
||||
</Show>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue