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