From 0f22906767e3afbfeb2b5bed035f6579cba9315c Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 21 Dec 2025 17:15:44 +0000 Subject: [PATCH] fix(ai): filter out noise from anomalies and status changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical changes to surface only actionable insights: 1. Anomalies now require at least 50% deviation from baseline - '1.0x baseline' values filtered out (statistically significant but not actionable) - Must be >1.5x above OR <0.5x below baseline to report 2. Status changes filter out startup noise - 'unknown → running' is just system starting, not a real state change - Backups removed from main list (they have dedicated section) 3. Only show genuinely interesting changes: - Config changes, migrations, restarts, deletions - Things that require operator attention This massively reduces noise while keeping high-signal alerts. --- .../src/components/shared/AIOverviewTable.tsx | 16 +++++++--- internal/ai/baseline/store.go | 31 ++++++++++++------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/frontend-modern/src/components/shared/AIOverviewTable.tsx b/frontend-modern/src/components/shared/AIOverviewTable.tsx index dd83d6d..872b1d6 100644 --- a/frontend-modern/src/components/shared/AIOverviewTable.tsx +++ b/frontend-modern/src/components/shared/AIOverviewTable.tsx @@ -291,14 +291,22 @@ export const AIOverviewTable: Component<{ showWhenEmpty?: boolean }> = (props) = } - // Changes - only show meaningful changes, not just "created" (infrastructure detection) - // Valuable changes: config, status, migrated, restarted, deleted, backed_up - // Skip: created (just means AI patrol discovered the resource) - const meaningfulChanges = changes().filter(c => c.change_type !== 'created'); + // Changes - only show meaningful changes, not noise + // Skip: created (just patrol discovering resources) + // Skip: backed_up (backups have their own section, no need to duplicate here) + // Skip: status changes from "unknown" (just startup, not real state changes) + const meaningfulChanges = changes().filter(c => { + if (c.change_type === 'created') return false; + if (c.change_type === 'backed_up') return false; + // Filter out startup noise: "unknown → running" or "unknown → stopped" + if (c.change_type === 'status' && c.description?.includes('unknown →')) return false; + return true; + }); const visibleChanges = showAllChanges() ? meaningfulChanges : meaningfulChanges.slice(0, CHANGE_LIMIT); + for (const change of visibleChanges) { const changeTypeBadgeClass: Record = { deleted: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300', diff --git a/internal/ai/baseline/store.go b/internal/ai/baseline/store.go index 1246463..a8870f9 100644 --- a/internal/ai/baseline/store.go +++ b/internal/ai/baseline/store.go @@ -291,33 +291,40 @@ func (s *Store) CheckResourceAnomalies(resourceID string, metrics map[string]flo for metric, value := range metrics { severity, zScore, baseline := s.CheckAnomaly(resourceID, metric, value) - if severity != AnomalyNone { + if severity != AnomalyNone && baseline != nil { + // Compute ratio: current value / baseline mean + ratio := value / baseline.Mean + + // Filter out statistically significant but practically meaningless anomalies + // Users don't care about "1.0x baseline" or small deviations + // Require at least 50% deviation from baseline to report + if ratio >= 0.5 && ratio <= 1.5 { + continue // Too close to baseline to be actionable + } + report := AnomalyReport{ ResourceID: resourceID, Metric: metric, CurrentValue: value, ZScore: zScore, Severity: severity, + BaselineMean: baseline.Mean, + BaselineStdDev: baseline.StdDev, } - if baseline != nil { - report.BaselineMean = baseline.Mean - report.BaselineStdDev = baseline.StdDev - - // Generate human-readable description - ratio := value / baseline.Mean - direction := "above" - if zScore < 0 { - direction = "below" - } - report.Description = formatAnomalyDescription(metric, ratio, direction, severity) + // Generate human-readable description + direction := "above" + if zScore < 0 { + direction = "below" } + report.Description = formatAnomalyDescription(metric, ratio, direction, severity) anomalies = append(anomalies, report) } } return anomalies + } // formatAnomalyDescription generates a human-readable anomaly description