From 0feb039775129f2e71a7111d1ff7dfa33ed2f230 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 25 Dec 2025 05:06:26 +0000 Subject: [PATCH] fix: Allow dismissing AI findings without Pro license. Related to #885 Users who accumulated AI patrol findings before the patrol-without-AI bug was fixed (24c4bb0b) could not dismiss them because the dismiss and resolve endpoints required a Pro license. Changes: - Remove Pro license requirement from /api/ai/patrol/dismiss endpoint - Remove Pro license requirement from /api/ai/patrol/resolve endpoint - Add ClearAll() method to FindingsStore for bulk clearing - Add DELETE /api/ai/patrol/findings endpoint for clearing all findings - Add "Clear All" button to AI Insights UI Users can now dismiss or resolve any findings they can see, and admins can clear all findings at once if needed. --- frontend-modern/src/api/patrol.ts | 12 ++++++ frontend-modern/src/pages/Alerts.tsx | 61 ++++++++++++++++++++-------- internal/ai/findings.go | 14 +++++++ internal/api/ai_handlers.go | 42 +++++++++++++++++++ internal/api/router.go | 18 ++++++-- 5 files changed, 127 insertions(+), 20 deletions(-) diff --git a/frontend-modern/src/api/patrol.ts b/frontend-modern/src/api/patrol.ts index 78facd21b9..3f08b2e5e7 100644 --- a/frontend-modern/src/api/patrol.ts +++ b/frontend-modern/src/api/patrol.ts @@ -243,6 +243,18 @@ export async function suppressFinding(findingId: string): Promise<{ success: boo }); } +/** + * Clear all AI findings + * Removes all accumulated findings from the store. + * Useful for users who want to start fresh or who accumulated findings + * before AI was properly configured. + */ +export async function clearAllFindings(): Promise<{ success: boolean; cleared: number; message: string }> { + return apiFetchJSON('/api/ai/patrol/findings?confirm=true', { + method: 'DELETE', + }); +} + /** * Severity color mapping for UI */ diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index e4ffa2e030..a37a9bdb48 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -32,7 +32,7 @@ import History from 'lucide-solid/icons/history'; import Gauge from 'lucide-solid/icons/gauge'; import Send from 'lucide-solid/icons/send'; import Calendar from 'lucide-solid/icons/calendar'; -import { getPatrolStatus, getFindings, getFindingsHistory, getPatrolRunHistory, forcePatrol, subscribeToPatrolStream, dismissFinding, suppressFinding, resolveFinding, getSuppressionRules, addSuppressionRule, deleteSuppressionRule, type Finding, type PatrolStatus, type PatrolRunRecord, type SuppressionRule, severityColors, formatTimestamp, categoryLabels } from '@/api/patrol'; +import { getPatrolStatus, getFindings, getFindingsHistory, getPatrolRunHistory, forcePatrol, subscribeToPatrolStream, dismissFinding, suppressFinding, resolveFinding, clearAllFindings, getSuppressionRules, addSuppressionRule, deleteSuppressionRule, type Finding, type PatrolStatus, type PatrolRunRecord, type SuppressionRule, severityColors, formatTimestamp, categoryLabels } from '@/api/patrol'; import { aiChatStore } from '@/stores/aiChat'; type AlertTab = 'overview' | 'thresholds' | 'destinations' | 'schedule' | 'history'; @@ -2703,22 +2703,49 @@ function OverviewTab(props: { size="md" class="mb-0" /> - +
+ 0}> + + + +
diff --git a/internal/ai/findings.go b/internal/ai/findings.go index 0fea4c4928..cb28133369 100644 --- a/internal/ai/findings.go +++ b/internal/ai/findings.go @@ -584,6 +584,20 @@ func (s *FindingsStore) GetAll(startTime *time.Time) []*Finding { return result } +// ClearAll removes all findings from the store +// Returns the number of findings removed +func (s *FindingsStore) ClearAll() int { + s.mu.Lock() + defer s.mu.Unlock() + + count := len(s.findings) + s.findings = make(map[string]*Finding) + s.byResource = make(map[string][]string) + s.activeCounts = make(map[FindingSeverity]int) + s.scheduleSave() + return count +} + // Cleanup removes old resolved findings func (s *FindingsStore) Cleanup(maxAge time.Duration) int { s.mu.Lock() diff --git a/internal/api/ai_handlers.go b/internal/api/ai_handlers.go index a39bf7720a..2b7a009496 100644 --- a/internal/api/ai_handlers.go +++ b/internal/api/ai_handlers.go @@ -2718,6 +2718,48 @@ func (h *AISettingsHandler) HandleSuppressFinding(w http.ResponseWriter, r *http } } +// HandleClearAllFindings clears all AI findings (DELETE /api/ai/patrol/findings) +// This allows users to clear accumulated findings, especially useful for users who +// accumulated findings before the patrol-without-AI bug was fixed. +func (h *AISettingsHandler) HandleClearAllFindings(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodDelete { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Require authentication (already wrapped by RequireAuth in router) + if !CheckAuth(h.config, w, r) { + return + } + + // Check for confirm parameter + if r.URL.Query().Get("confirm") != "true" { + http.Error(w, "confirm=true query parameter required", http.StatusBadRequest) + return + } + + patrol := h.aiService.GetPatrolService() + if patrol == nil { + http.Error(w, "Patrol service not available", http.StatusServiceUnavailable) + return + } + + findings := patrol.GetFindings() + count := findings.ClearAll() + + log.Info().Int("count", count).Msg("Cleared all AI findings") + + response := map[string]interface{}{ + "success": true, + "cleared": count, + "message": fmt.Sprintf("Cleared %d findings", count), + } + + if err := utils.WriteJSONResponse(w, response); err != nil { + log.Error().Err(err).Msg("Failed to write clear findings response") + } +} + // HandleGetFindingsHistory returns all findings including resolved for history (GET /api/ai/patrol/history) func (h *AISettingsHandler) HandleGetFindingsHistory(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { diff --git a/internal/api/router.go b/internal/api/router.go index 6f36df9e9f..30b2a721f2 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1161,14 +1161,26 @@ func (r *Router) setupRoutes() { // Mutation endpoints (run, acknowledge, dismiss, etc.) return 402 to prevent unauthorized actions r.mux.HandleFunc("/api/ai/patrol/status", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatrolStatus)) r.mux.HandleFunc("/api/ai/patrol/stream", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandlePatrolStream))) - r.mux.HandleFunc("/api/ai/patrol/findings", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatrolFindings)) + r.mux.HandleFunc("/api/ai/patrol/findings", RequireAuth(r.config, func(w http.ResponseWriter, req *http.Request) { + switch req.Method { + case http.MethodGet: + r.aiSettingsHandler.HandleGetPatrolFindings(w, req) + case http.MethodDelete: + // Clear all findings - doesn't require Pro license so users can clean up accumulated findings + r.aiSettingsHandler.HandleClearAllFindings(w, req) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + })) r.mux.HandleFunc("/api/ai/patrol/history", RequireAuth(r.config, r.aiSettingsHandler.HandleGetFindingsHistory)) r.mux.HandleFunc("/api/ai/patrol/run", RequireAdmin(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleForcePatrol))) r.mux.HandleFunc("/api/ai/patrol/acknowledge", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleAcknowledgeFinding))) - r.mux.HandleFunc("/api/ai/patrol/dismiss", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleDismissFinding))) + // Dismiss and resolve don't require Pro license - users should be able to clear findings they can see + // This is especially important for users who accumulated findings before fixing the patrol-without-AI bug + r.mux.HandleFunc("/api/ai/patrol/dismiss", RequireAuth(r.config, r.aiSettingsHandler.HandleDismissFinding)) r.mux.HandleFunc("/api/ai/patrol/suppress", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleSuppressFinding))) r.mux.HandleFunc("/api/ai/patrol/snooze", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleSnoozeFinding))) - r.mux.HandleFunc("/api/ai/patrol/resolve", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleResolveFinding))) + r.mux.HandleFunc("/api/ai/patrol/resolve", RequireAuth(r.config, r.aiSettingsHandler.HandleResolveFinding)) r.mux.HandleFunc("/api/ai/patrol/runs", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatrolRunHistory)) // Suppression rules management (also Pro-only since they control LLM behavior) // GET returns empty array for unlicensed, POST returns 402