From a535b228497286c1eb56b4c7c059a3a7da339b98 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 19 Dec 2025 17:04:14 +0000 Subject: [PATCH] fix(ai): improve patrol timing accuracy and status reporting --- frontend-modern/src/api/patrol.ts | 5 + .../src/components/AI/AIStatusIndicator.tsx | 12 +- .../src/components/shared/SettingsPanel.tsx | 2 +- frontend-modern/src/pages/Alerts.tsx | 213 +++++++++++------- internal/api/ai_handlers.go | 23 +- 5 files changed, 171 insertions(+), 84 deletions(-) diff --git a/frontend-modern/src/api/patrol.ts b/frontend-modern/src/api/patrol.ts index b927a51..9a64dc9 100644 --- a/frontend-modern/src/api/patrol.ts +++ b/frontend-modern/src/api/patrol.ts @@ -41,6 +41,8 @@ export interface FindingsSummary { info: number; } +export type LicenseStatus = 'none' | 'active' | 'expired' | 'grace_period'; + export interface PatrolStatus { running: boolean; enabled: boolean; @@ -53,6 +55,9 @@ export interface PatrolStatus { error_count: number; healthy: boolean; interval_ms: number; // Patrol interval in milliseconds + license_required?: boolean; + license_status?: LicenseStatus; + upgrade_url?: string; summary: FindingsSummary; } diff --git a/frontend-modern/src/components/AI/AIStatusIndicator.tsx b/frontend-modern/src/components/AI/AIStatusIndicator.tsx index 61484d4..34e4842 100644 --- a/frontend-modern/src/components/AI/AIStatusIndicator.tsx +++ b/frontend-modern/src/components/AI/AIStatusIndicator.tsx @@ -49,7 +49,17 @@ export function AIStatusIndicator() { const tooltipText = createMemo(() => { const s = status(); - if (!s || !s.enabled) return 'AI Patrol disabled'; + if (!s) return 'AI Patrol status unavailable'; + if (!s.enabled) return 'AI Patrol disabled'; + if (s.license_required) { + if (s.license_status === 'active') { + return 'AI Patrol is not included in this license tier'; + } + if (s.license_status === 'expired') { + return 'AI Patrol license expired - upgrade to restore'; + } + return 'AI Patrol requires Pulse Pro'; + } if (!s.running) return 'AI Patrol not running'; const parts: string[] = []; diff --git a/frontend-modern/src/components/shared/SettingsPanel.tsx b/frontend-modern/src/components/shared/SettingsPanel.tsx index ef1c36a..f769e23 100644 --- a/frontend-modern/src/components/shared/SettingsPanel.tsx +++ b/frontend-modern/src/components/shared/SettingsPanel.tsx @@ -9,7 +9,7 @@ type SettingsPanelProps = { bodyClass?: string; tone?: 'default' | 'muted' | 'info' | 'success' | 'warning' | 'danger'; padding?: 'none' | 'sm' | 'md' | 'lg'; -} & JSX.HTMLAttributes; +} & Omit, 'title'>; export function SettingsPanel(props: SettingsPanelProps) { const [local, rest] = splitProps(props, [ diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx index 013d0bd..308ddc5 100644 --- a/frontend-modern/src/pages/Alerts.tsx +++ b/frontend-modern/src/pages/Alerts.tsx @@ -2088,6 +2088,25 @@ function OverviewTab(props: { const [liveStreamBlocks, setLiveStreamBlocks] = createSignal([]); const [currentThinking, setCurrentThinking] = createSignal(''); let liveStreamUnsubscribe: (() => void) | null = null; + const patrolRequiresLicense = createMemo(() => patrolStatus()?.license_required === true); + const patrolUpgradeURL = createMemo(() => patrolStatus()?.upgrade_url || 'https://pulsemonitor.app/pro'); + const patrolLicenseNote = createMemo(() => { + if (!patrolRequiresLicense()) return ''; + const status = patrolStatus()?.license_status; + if (status === 'active') { + return 'Your license is active but does not include AI Patrol.'; + } + if (status === 'expired') { + return 'Your license has expired. Renew to restore AI Patrol.'; + } + if (status === 'none') { + return 'No Pulse Pro license is active.'; + } + if (status === 'grace_period') { + return 'Your license is in the grace period.'; + } + return 'AI Patrol insights require Pulse Pro.'; + }); // Effect to manage live stream subscription when expanded createEffect(() => { @@ -2460,8 +2479,10 @@ function OverviewTab(props: { + +
+
+ + + +
+

Pulse Pro required

+

+ {patrolLicenseNote() || 'AI Patrol insights require Pulse Pro.'} +

+ + Upgrade to Pulse Pro + +
+
+
+
+ {/* Summary Stats Bar */}
@@ -2506,34 +2551,35 @@ function OverviewTab(props: {
-
- 0} - fallback={ -
-
- - - - + +
+ 0} + fallback={ +
+
+ + + + +
+

All Systems Healthy

+

AI patrol found no issues to report

-

All Systems Healthy

-

AI patrol found no issues to report

-
- } - > - !pendingFixFindings().has(f.id))}> - {(finding) => { - const colors = severityColors[finding.severity]; - const [isExpanded, setIsExpanded] = createSignal(false); - return ( -
+ } + > + !pendingFixFindings().has(f.id))}> + {(finding) => { + const colors = severityColors[finding.severity]; + const [isExpanded, setIsExpanded] = createSignal(false); + return ( +
{/* Compact header - always visible, clickable */}
- ); - }} - - -
- - {/* Suppression Rules - What's being ignored */} -
-
- - + ); + }} + +
+ + + + {/* Suppression Rules - What's being ignored */} +
+
+ + +
{/* Add rule form */} @@ -2921,29 +2969,31 @@ function OverviewTab(props: { )}
-
-
+ +
+
- {/* Patrol Check History */} -
-
- + + + + Patrol Check History ({filteredPatrolHistory().length} runs) + 0 && patrolRunHistory()[0].status !== 'healthy'}> + Issues Found + + {/* Next Patrol Timer */} @@ -3303,8 +3353,9 @@ function OverviewTab(props: {
- -
+ +
+
diff --git a/internal/api/ai_handlers.go b/internal/api/ai_handlers.go index 9d35ccb..4f3af28 100644 --- a/internal/api/ai_handlers.go +++ b/internal/api/ai_handlers.go @@ -20,6 +20,7 @@ import ( "github.com/rcourtman/pulse-go-rewrite/internal/ai/cost" "github.com/rcourtman/pulse-go-rewrite/internal/ai/providers" "github.com/rcourtman/pulse-go-rewrite/internal/config" + "github.com/rcourtman/pulse-go-rewrite/internal/license" "github.com/rcourtman/pulse-go-rewrite/internal/utils" "github.com/rs/zerolog/log" ) @@ -136,6 +137,11 @@ func (h *AISettingsHandler) GetAlertTriggeredAnalyzer() *ai.AlertTriggeredAnalyz return h.aiService.GetAlertTriggeredAnalyzer() } +// SetLicenseChecker sets the license checker for Pro feature gating +func (h *AISettingsHandler) SetLicenseChecker(checker ai.LicenseChecker) { + h.aiService.SetLicenseChecker(checker) +} + // AISettingsResponse is returned by GET /api/settings/ai // API keys are masked for security type AISettingsResponse struct { @@ -1957,7 +1963,11 @@ type PatrolStatusResponse struct { FindingsCount int `json:"findings_count"` Healthy bool `json:"healthy"` IntervalMs int64 `json:"interval_ms"` // Patrol interval in milliseconds - Summary struct { + // License status for Pro feature gating + LicenseRequired bool `json:"license_required"` // True if Pro license needed for full features + LicenseStatus string `json:"license_status"` // "active", "expired", "grace_period", "none" + UpgradeURL string `json:"upgrade_url,omitempty"` + Summary struct { Critical int `json:"critical"` Warning int `json:"warning"` Watch int `json:"watch"` @@ -1989,6 +1999,12 @@ func (h *AISettingsHandler) HandleGetPatrolStatus(w http.ResponseWriter, r *http status := patrol.GetStatus() summary := patrol.GetFindingsSummary() + // Determine license status for Pro feature gating + // GetLicenseState returns accurate state: none, active, expired, grace_period + licenseStatus, _ := h.aiService.GetLicenseState() + // Check specifically for ai_patrol feature using canonical license constant + hasPatrolFeature := h.aiService.HasLicenseFeature(license.FeatureAIPatrol) + response := PatrolStatusResponse{ Running: status.Running, Enabled: h.aiService.IsEnabled(), @@ -1999,6 +2015,11 @@ func (h *AISettingsHandler) HandleGetPatrolStatus(w http.ResponseWriter, r *http FindingsCount: status.FindingsCount, Healthy: status.Healthy, IntervalMs: status.IntervalMs, + LicenseRequired: !hasPatrolFeature, + LicenseStatus: licenseStatus, + } + if !hasPatrolFeature { + response.UpgradeURL = "https://pulsemonitor.app/pro" } response.Summary.Critical = summary.Critical response.Summary.Warning = summary.Warning