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)
This commit is contained in:
parent
d03b3d644c
commit
1a78e8846a
3 changed files with 116 additions and 29 deletions
|
|
@ -69,6 +69,9 @@ export const AISettings: Component = () => {
|
|||
const [testingProvider, setTestingProvider] = createSignal<string | null>(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 = () => {
|
|||
</p>
|
||||
</div>
|
||||
|
||||
{/* Patrol Schedule Preset */}
|
||||
{/* Patrol Interval */}
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
Background Patrol Frequency
|
||||
Background Patrol Interval (minutes)
|
||||
</label>
|
||||
<select
|
||||
class={controlClass()}
|
||||
value={form.patrolSchedulePreset}
|
||||
onChange={(e) => setForm('patrolSchedulePreset', e.currentTarget.value)}
|
||||
disabled={saving()}
|
||||
>
|
||||
<option value="15min">Every 15 minutes (high token usage)</option>
|
||||
<option value="1hr">Every hour</option>
|
||||
<option value="6hr">Every 6 hours (recommended)</option>
|
||||
<option value="12hr">Every 12 hours</option>
|
||||
<option value="daily">Once daily</option>
|
||||
<option value="disabled">Disabled (use alert-triggered only)</option>
|
||||
</select>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
class={controlClass()}
|
||||
value={form.patrolIntervalMinutes}
|
||||
onInput={(e) => {
|
||||
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' }}
|
||||
/>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{form.patrolIntervalMinutes === 0
|
||||
? 'Disabled'
|
||||
: form.patrolIntervalMinutes >= 60
|
||||
? `${Math.floor(form.patrolIntervalMinutes / 60)}h ${form.patrolIntervalMinutes % 60 > 0 ? `${form.patrolIntervalMinutes % 60}m` : ''}`
|
||||
: `${form.patrolIntervalMinutes}m`}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
How often to scan all infrastructure for potential issues
|
||||
Set to 0 to disable scheduled patrol. Minimum 10 minutes when enabled.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -983,10 +998,59 @@ export const AISettings: Component = () => {
|
|||
</div>
|
||||
<Toggle
|
||||
checked={form.patrolAutoFix}
|
||||
onChange={(event) => 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())}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Auto-Fix Warning & Acknowledgement */}
|
||||
<Show when={!form.patrolAutoFix}>
|
||||
<div class="mt-3 p-3 bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-lg">
|
||||
<div class="flex gap-2">
|
||||
<svg class="w-4 h-4 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<div class="text-xs text-amber-800 dark:text-amber-200">
|
||||
<p class="font-semibold mb-1">Before enabling Auto-Fix Mode:</p>
|
||||
<ul class="list-disc pl-4 space-y-0.5 mb-2">
|
||||
<li>AI will execute commands <strong>without asking for approval</strong></li>
|
||||
<li>Actions may be <strong>irreversible</strong> (e.g., restarting services, clearing caches)</li>
|
||||
<li>Recommended to test in non-production environments first</li>
|
||||
</ul>
|
||||
<label class="flex items-center gap-2 cursor-pointer mt-2 pt-2 border-t border-amber-200 dark:border-amber-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoFixAcknowledged()}
|
||||
onChange={(e) => setAutoFixAcknowledged(e.currentTarget.checked)}
|
||||
class="w-4 h-4 rounded border-amber-400 text-amber-600 focus:ring-amber-500"
|
||||
/>
|
||||
<span class="font-medium">I understand the risks and want to enable Auto-Fix</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Warning when enabled */}
|
||||
<Show when={form.patrolAutoFix}>
|
||||
<div class="mt-3 p-3 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg">
|
||||
<div class="flex gap-2">
|
||||
<svg class="w-4 h-4 text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<div class="text-xs text-red-800 dark:text-red-200">
|
||||
<p class="font-semibold">⚠️ Auto-Fix is ENABLED</p>
|
||||
<p class="mt-1">AI patrol will automatically attempt to fix issues without asking for approval. Review findings regularly.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue