From 783537e1d2f575c466e3797341be465dabf28338 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 21 Dec 2025 11:36:54 +0000 Subject: [PATCH] feat(ai): make anomaly detection FREE and add learning status endpoint Free Features (no license required): - Anomaly detection - removed license gating, purely statistical analysis - Learning status endpoint - GET /api/ai/intelligence/learning Learning Status Response: - resources_baselined: count of resources with learned baselines - total_metrics: total metric baselines (cpu + memory + disk) - metric_breakdown: {cpu: X, memory: Y, disk: Z} - status: 'waiting' | 'learning' | 'active' - message: human-readable description This makes the AI intelligence features visible to all users, encouraging upgrades for the full LLM-powered patrol experience. --- frontend-modern/src/api/ai.ts | 8 ++ frontend-modern/src/types/aiIntelligence.ts | 17 ++++ internal/api/ai_intelligence_handlers.go | 87 ++++++++++++++++++--- internal/api/router.go | 1 + 4 files changed, 101 insertions(+), 12 deletions(-) diff --git a/frontend-modern/src/api/ai.ts b/frontend-modern/src/api/ai.ts index 5b6555f..f1f9a39 100644 --- a/frontend-modern/src/api/ai.ts +++ b/frontend-modern/src/api/ai.ts @@ -17,10 +17,12 @@ import type { BaselinesResponse, RemediationsResponse, AnomaliesResponse, + LearningStatusResponse, IntelligenceSummary, ResourceIntelligence, } from '@/types/aiIntelligence'; + export class AIAPI { private static baseUrl = '/api'; @@ -129,6 +131,12 @@ export class AIAPI { return apiFetchJSON(`${this.baseUrl}/ai/intelligence/anomalies${params}`) as Promise; } + // Get learning/baseline status (FREE - no license required) + // Shows how many resources have been baselined and the overall learning state + static async getLearningStatus(): Promise { + return apiFetchJSON(`${this.baseUrl}/ai/intelligence/learning`) as Promise; + } + // ============================================ // Unified Intelligence API // Aggregates all AI subsystems into a single view diff --git a/frontend-modern/src/types/aiIntelligence.ts b/frontend-modern/src/types/aiIntelligence.ts index ca137b2..fcf9b88 100644 --- a/frontend-modern/src/types/aiIntelligence.ts +++ b/frontend-modern/src/types/aiIntelligence.ts @@ -153,6 +153,23 @@ export interface AnomaliesResponse extends LicenseGatedResponse { }; } +// Learning/baseline status for showing system intelligence state +export type LearningStatus = 'waiting' | 'learning' | 'active'; + +export interface LearningStatusResponse { + resources_baselined: number; + total_metrics: number; + metric_breakdown: { + cpu?: number; + memory?: number; + disk?: number; + }; + status: LearningStatus; + message: string; + license_required: boolean; +} + + // ============================================================================ // Unified Intelligence Types (for /api/ai/intelligence endpoint) // ============================================================================ diff --git a/internal/api/ai_intelligence_handlers.go b/internal/api/ai_intelligence_handlers.go index 9a46bba..e03c7c0 100644 --- a/internal/api/ai_intelligence_handlers.go +++ b/internal/api/ai_intelligence_handlers.go @@ -722,13 +722,6 @@ func (h *AISettingsHandler) HandleGetAnomalies(w http.ResponseWriter, r *http.Re } } - // License gating - locked := !h.aiService.HasLicenseFeature(license.FeatureAIPatrol) - if locked { - w.Header().Set("X-License-Required", "true") - w.Header().Set("X-License-Feature", license.FeatureAIPatrol) - } - count := len(result) // Count by severity for summary @@ -744,18 +737,88 @@ func (h *AISettingsHandler) HandleGetAnomalies(w http.ResponseWriter, r *http.Re } } - if locked { - result = []map[string]interface{}{} - } + // NOTE: Anomaly detection is FREE (no license required) + // It's purely deterministic statistical analysis with no LLM costs + // This provides value to all users and encourages Pro upgrades for patrol if err := utils.WriteJSONResponse(w, map[string]interface{}{ "anomalies": result, "count": count, "severity_counts": severityCounts, - "license_required": locked, - "upgrade_url": aiIntelligenceUpgradeURL, + "license_required": false, }); err != nil { log.Error().Err(err).Msg("Failed to write anomalies response") } } +// HandleGetLearningStatus returns the current state of baseline learning (GET /api/ai/intelligence/learning) +// This is FREE (no license required) and shows users how much the system has learned +func (h *AISettingsHandler) HandleGetLearningStatus(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + patrol := h.aiService.GetPatrolService() + if patrol == nil { + if err := utils.WriteJSONResponse(w, map[string]interface{}{ + "resources_baselined": 0, + "total_metrics": 0, + "status": "patrol_not_initialized", + "message": "Baseline learning requires AI Patrol to be initialized", + }); err != nil { + log.Error().Err(err).Msg("Failed to write learning status response") + } + return + } + + baselineStore := patrol.GetBaselineStore() + if baselineStore == nil { + if err := utils.WriteJSONResponse(w, map[string]interface{}{ + "resources_baselined": 0, + "total_metrics": 0, + "status": "baseline_store_not_initialized", + "message": "Baseline store not yet initialized", + }); err != nil { + log.Error().Err(err).Msg("Failed to write learning status response") + } + return + } + + // Get all baselines and count metrics + baselines := baselineStore.GetAllBaselines() + resourceCount := baselineStore.ResourceCount() + + // Count unique resources and total metrics + resourceIDs := make(map[string]bool) + totalMetrics := 0 + metricCounts := make(map[string]int) // cpu, memory, disk counts + + for _, baseline := range baselines { + resourceIDs[baseline.ResourceID] = true + totalMetrics++ + metricCounts[baseline.Metric]++ + } + + // Determine status + status := "learning" + message := "Actively learning baseline patterns" + if resourceCount == 0 { + status = "waiting" + message = "Waiting for metric data to learn from" + } else if resourceCount >= 5 { + status = "active" + message = "Baselines established and anomaly detection is active" + } + + if err := utils.WriteJSONResponse(w, map[string]interface{}{ + "resources_baselined": resourceCount, + "total_metrics": totalMetrics, + "metric_breakdown": metricCounts, + "status": status, + "message": message, + "license_required": false, + }); err != nil { + log.Error().Err(err).Msg("Failed to write learning status response") + } +} diff --git a/internal/api/router.go b/internal/api/router.go index bad16ae..fad676c 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1217,6 +1217,7 @@ func (r *Router) setupRoutes() { r.mux.HandleFunc("/api/ai/intelligence/baselines", RequireAuth(r.config, r.aiSettingsHandler.HandleGetBaselines)) r.mux.HandleFunc("/api/ai/intelligence/remediations", RequireAuth(r.config, r.aiSettingsHandler.HandleGetRemediations)) r.mux.HandleFunc("/api/ai/intelligence/anomalies", RequireAuth(r.config, r.aiSettingsHandler.HandleGetAnomalies)) + r.mux.HandleFunc("/api/ai/intelligence/learning", RequireAuth(r.config, r.aiSettingsHandler.HandleGetLearningStatus)) // Agent WebSocket for AI command execution r.mux.HandleFunc("/api/agent/ws", r.handleAgentWebSocket)