fix(ai): filter out noise from anomalies and status changes

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.
This commit is contained in:
rcourtman 2025-12-21 17:15:44 +00:00
parent 5138b01ed8
commit 0f22906767
2 changed files with 31 additions and 16 deletions

View file

@ -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<string, string> = {
deleted: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',

View file

@ -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