From 1a78e8846a33b04a8f3e5f4c9e10c0467394a9d5 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 11 Dec 2025 23:24:33 +0000 Subject: [PATCH] feat(ai): Replace patrol frequency dropdown with custom minutes input - Changed patrol schedule from preset dropdown to freeform number input - Users can now set any interval (min 10 minutes, max 7 days, or 0 to disable) - Added patrol_interval_minutes to API request/response (preset is now deprecated) - Backend validates: min 10 minutes when enabled, max 10080 (7 days) - Frontend shows human-readable duration next to input (e.g., '6h', '2h 30m') Also improved Auto-Fix Mode safety: - Removed '(recommended)' from preset options (was subjective) - Added 'I understand the risks' acknowledgement checkbox - Toggle is disabled until user explicitly acknowledges the risks - Shows prominent warning when Auto-Fix is enabled - Acknowledgement is session-based (must re-acknowledge on page reload) --- .../src/components/Settings/AISettings.tsx | 110 ++++++++++++++---- frontend-modern/src/types/ai.ts | 6 +- internal/api/ai_handlers.go | 29 ++++- 3 files changed, 116 insertions(+), 29 deletions(-) diff --git a/frontend-modern/src/components/Settings/AISettings.tsx b/frontend-modern/src/components/Settings/AISettings.tsx index d7027c7..0447d60 100644 --- a/frontend-modern/src/components/Settings/AISettings.tsx +++ b/frontend-modern/src/components/Settings/AISettings.tsx @@ -69,6 +69,9 @@ export const AISettings: Component = () => { const [testingProvider, setTestingProvider] = createSignal(null); const [providerTestResult, setProviderTestResult] = createSignal<{ provider: string; success: boolean; message: string } | null>(null); + // Auto-fix acknowledgement state (not persisted - must acknowledge each session) + const [autoFixAcknowledged, setAutoFixAcknowledged] = createSignal(false); + const [form, setForm] = createStore({ enabled: false, provider: 'anthropic' as AIProvider, // Legacy - kept for compatibility @@ -80,7 +83,7 @@ export const AISettings: Component = () => { clearApiKey: false, autonomousMode: false, authMethod: 'api_key' as AuthMethod, - patrolSchedulePreset: '6hr', + patrolIntervalMinutes: 360, // 6 hours default alertTriggeredAnalysis: true, patrolAutoFix: false, // Multi-provider credentials @@ -104,7 +107,7 @@ export const AISettings: Component = () => { clearApiKey: false, autonomousMode: false, authMethod: 'api_key', - patrolSchedulePreset: '6hr', + patrolIntervalMinutes: 360, // 6 hours default alertTriggeredAnalysis: true, patrolAutoFix: false, // Multi-provider - empty by default @@ -128,7 +131,7 @@ export const AISettings: Component = () => { clearApiKey: false, autonomousMode: data.autonomous_mode || false, authMethod: data.auth_method || 'api_key', - patrolSchedulePreset: data.patrol_schedule_preset || '6hr', + patrolIntervalMinutes: data.patrol_interval_minutes ?? 360, // Use minutes, default to 6hr alertTriggeredAnalysis: data.alert_triggered_analysis !== false, // default to true patrolAutoFix: data.patrol_auto_fix || false, // default to false (observe only) // Multi-provider - never load actual keys from server (security), just track if configured @@ -249,8 +252,8 @@ export const AISettings: Component = () => { } // Include patrol settings if changed - if (form.patrolSchedulePreset !== settings()?.patrol_schedule_preset) { - payload.patrol_schedule_preset = form.patrolSchedulePreset; + if (form.patrolIntervalMinutes !== (settings()?.patrol_interval_minutes ?? 360)) { + payload.patrol_interval_minutes = form.patrolIntervalMinutes; } if (form.alertTriggeredAnalysis !== settings()?.alert_triggered_analysis) { @@ -922,27 +925,39 @@ export const AISettings: Component = () => {

- {/* Patrol Schedule Preset */} + {/* Patrol Interval */}
- +
+ { + const value = parseInt(e.currentTarget.value, 10); + if (!isNaN(value)) { + setForm('patrolIntervalMinutes', Math.max(0, value)); + } + }} + min={0} + max={10080} + step={15} + disabled={saving()} + style={{ width: '120px' }} + /> + + {form.patrolIntervalMinutes === 0 + ? 'Disabled' + : form.patrolIntervalMinutes >= 60 + ? `${Math.floor(form.patrolIntervalMinutes / 60)}h ${form.patrolIntervalMinutes % 60 > 0 ? `${form.patrolIntervalMinutes % 60}m` : ''}` + : `${form.patrolIntervalMinutes}m`} + +

- How often to scan all infrastructure for potential issues + Set to 0 to disable scheduled patrol. Minimum 10 minutes when enabled.

@@ -983,10 +998,59 @@ export const AISettings: Component = () => {
setForm('patrolAutoFix', event.currentTarget.checked)} - disabled={saving()} + onChange={(event) => { + // Can only enable if acknowledged + if (event.currentTarget.checked && !autoFixAcknowledged()) { + return; // Prevent enabling without acknowledgement + } + setForm('patrolAutoFix', event.currentTarget.checked); + }} + disabled={saving() || (!form.patrolAutoFix && !autoFixAcknowledged())} /> + + {/* Auto-Fix Warning & Acknowledgement */} + +
+
+ + + +
+

Before enabling Auto-Fix Mode:

+
    +
  • AI will execute commands without asking for approval
  • +
  • Actions may be irreversible (e.g., restarting services, clearing caches)
  • +
  • Recommended to test in non-production environments first
  • +
+ +
+
+
+
+ + {/* Warning when enabled */} + +
+
+ + + +
+

⚠️ Auto-Fix is ENABLED

+

AI patrol will automatically attempt to fix issues without asking for approval. Review findings regularly.

+
+
+
+
diff --git a/frontend-modern/src/types/ai.ts b/frontend-modern/src/types/ai.ts index 60adffe..68bb181 100644 --- a/frontend-modern/src/types/ai.ts +++ b/frontend-modern/src/types/ai.ts @@ -25,7 +25,8 @@ export interface AISettings { auth_method: AuthMethod; // "api_key" or "oauth" oauth_connected: boolean; // true if OAuth tokens are configured // Patrol settings for token efficiency - patrol_schedule_preset?: string; // "15min" | "1hr" | "6hr" | "12hr" | "daily" | "disabled" + patrol_schedule_preset?: string; // DEPRECATED: use patrol_interval_minutes + patrol_interval_minutes?: number; // Patrol interval in minutes (0 = disabled, minimum 10) alert_triggered_analysis?: boolean; // true if AI should analyze when alerts fire patrol_auto_fix?: boolean; // true if patrol can attempt automatic remediation available_models?: ModelInfo[]; // DEPRECATED: use /api/ai/models endpoint @@ -52,7 +53,8 @@ export interface AISettingsUpdateRequest { chat_model?: string; // Model for interactive chat patrol_model?: string; // Model for background patrol // Patrol settings for token efficiency - patrol_schedule_preset?: string; // "15min" | "1hr" | "6hr" | "12hr" | "daily" | "disabled" + patrol_schedule_preset?: string; // DEPRECATED: use patrol_interval_minutes + patrol_interval_minutes?: number; // Custom interval in minutes (0 = disabled, minimum 10) alert_triggered_analysis?: boolean; // true if AI should analyze when alerts fire patrol_auto_fix?: boolean; // true if patrol can attempt automatic remediation // Multi-provider credentials diff --git a/internal/api/ai_handlers.go b/internal/api/ai_handlers.go index a5e254d..4b0ccee 100644 --- a/internal/api/ai_handlers.go +++ b/internal/api/ai_handlers.go @@ -118,7 +118,8 @@ type AISettingsResponse struct { AuthMethod string `json:"auth_method"` // "api_key" or "oauth" OAuthConnected bool `json:"oauth_connected"` // true if OAuth tokens are configured // Patrol settings for token efficiency - PatrolSchedulePreset string `json:"patrol_schedule_preset"` // "15min", "1hr", "6hr", "12hr", "daily", "disabled" + PatrolSchedulePreset string `json:"patrol_schedule_preset"` // DEPRECATED: legacy preset + PatrolIntervalMinutes int `json:"patrol_interval_minutes"` // Patrol interval in minutes (0 = disabled) AlertTriggeredAnalysis bool `json:"alert_triggered_analysis"` // true if AI analyzes when alerts fire AvailableModels []config.ModelInfo `json:"available_models"` // List of models for current provider // Multi-provider credentials - shows which providers are configured @@ -144,7 +145,8 @@ type AISettingsUpdateRequest struct { CustomContext *string `json:"custom_context,omitempty"` // user-provided infrastructure context AuthMethod *string `json:"auth_method,omitempty"` // "api_key" or "oauth" // Patrol settings for token efficiency - PatrolSchedulePreset *string `json:"patrol_schedule_preset,omitempty"` // "15min", "1hr", "6hr", "12hr", "daily", "disabled" + PatrolSchedulePreset *string `json:"patrol_schedule_preset,omitempty"` // DEPRECATED: use patrol_interval_minutes + PatrolIntervalMinutes *int `json:"patrol_interval_minutes,omitempty"` // Custom interval in minutes (0 = disabled, minimum 10) AlertTriggeredAnalysis *bool `json:"alert_triggered_analysis,omitempty"` // true if AI analyzes when alerts fire // Multi-provider credentials AnthropicAPIKey *string `json:"anthropic_api_key,omitempty"` // Set Anthropic API key @@ -198,6 +200,7 @@ func (h *AISettingsHandler) HandleGetAISettings(w http.ResponseWriter, r *http.R OAuthConnected: settings.OAuthAccessToken != "", // Patrol settings PatrolSchedulePreset: settings.PatrolSchedulePreset, + PatrolIntervalMinutes: settings.PatrolIntervalMinutes, AlertTriggeredAnalysis: settings.AlertTriggeredAnalysis, AvailableModels: nil, // Now populated via /api/ai/models endpoint // Multi-provider configuration @@ -321,8 +324,25 @@ func (h *AISettingsHandler) HandleUpdateAISettings(w http.ResponseWriter, r *htt settings.Enabled = *req.Enabled } - // Handle patrol schedule preset - if req.PatrolSchedulePreset != nil { + // Handle patrol interval - prefer custom minutes over preset + if req.PatrolIntervalMinutes != nil { + minutes := *req.PatrolIntervalMinutes + if minutes < 0 { + http.Error(w, "patrol_interval_minutes cannot be negative", http.StatusBadRequest) + return + } + if minutes > 0 && minutes < 10 { + http.Error(w, "patrol_interval_minutes must be at least 10 minutes (or 0 to disable)", http.StatusBadRequest) + return + } + if minutes > 10080 { // 7 days max + http.Error(w, "patrol_interval_minutes cannot exceed 10080 (7 days)", http.StatusBadRequest) + return + } + settings.PatrolIntervalMinutes = minutes + settings.PatrolSchedulePreset = "" // Clear preset when using custom minutes + } else if req.PatrolSchedulePreset != nil { + // Legacy preset support preset := strings.ToLower(strings.TrimSpace(*req.PatrolSchedulePreset)) switch preset { case "15min", "1hr", "6hr", "12hr", "daily", "disabled": @@ -414,6 +434,7 @@ func (h *AISettingsHandler) HandleUpdateAISettings(w http.ResponseWriter, r *htt AuthMethod: authMethod, OAuthConnected: settings.OAuthAccessToken != "", PatrolSchedulePreset: settings.PatrolSchedulePreset, + PatrolIntervalMinutes: settings.PatrolIntervalMinutes, AlertTriggeredAnalysis: settings.AlertTriggeredAnalysis, AvailableModels: nil, // Now populated via /api/ai/models endpoint // Multi-provider configuration