fix(ai): improve patrol timing accuracy and status reporting
This commit is contained in:
parent
3b55639fbe
commit
a535b22849
5 changed files with 171 additions and 84 deletions
|
|
@ -41,6 +41,8 @@ export interface FindingsSummary {
|
||||||
info: number;
|
info: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LicenseStatus = 'none' | 'active' | 'expired' | 'grace_period';
|
||||||
|
|
||||||
export interface PatrolStatus {
|
export interface PatrolStatus {
|
||||||
running: boolean;
|
running: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|
@ -53,6 +55,9 @@ export interface PatrolStatus {
|
||||||
error_count: number;
|
error_count: number;
|
||||||
healthy: boolean;
|
healthy: boolean;
|
||||||
interval_ms: number; // Patrol interval in milliseconds
|
interval_ms: number; // Patrol interval in milliseconds
|
||||||
|
license_required?: boolean;
|
||||||
|
license_status?: LicenseStatus;
|
||||||
|
upgrade_url?: string;
|
||||||
summary: FindingsSummary;
|
summary: FindingsSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,17 @@ export function AIStatusIndicator() {
|
||||||
|
|
||||||
const tooltipText = createMemo(() => {
|
const tooltipText = createMemo(() => {
|
||||||
const s = status();
|
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';
|
if (!s.running) return 'AI Patrol not running';
|
||||||
|
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ type SettingsPanelProps = {
|
||||||
bodyClass?: string;
|
bodyClass?: string;
|
||||||
tone?: 'default' | 'muted' | 'info' | 'success' | 'warning' | 'danger';
|
tone?: 'default' | 'muted' | 'info' | 'success' | 'warning' | 'danger';
|
||||||
padding?: 'none' | 'sm' | 'md' | 'lg';
|
padding?: 'none' | 'sm' | 'md' | 'lg';
|
||||||
} & JSX.HTMLAttributes<HTMLDivElement>;
|
} & Omit<JSX.HTMLAttributes<HTMLDivElement>, 'title'>;
|
||||||
|
|
||||||
export function SettingsPanel(props: SettingsPanelProps) {
|
export function SettingsPanel(props: SettingsPanelProps) {
|
||||||
const [local, rest] = splitProps(props, [
|
const [local, rest] = splitProps(props, [
|
||||||
|
|
|
||||||
|
|
@ -2088,6 +2088,25 @@ function OverviewTab(props: {
|
||||||
const [liveStreamBlocks, setLiveStreamBlocks] = createSignal<StreamBlock[]>([]);
|
const [liveStreamBlocks, setLiveStreamBlocks] = createSignal<StreamBlock[]>([]);
|
||||||
const [currentThinking, setCurrentThinking] = createSignal('');
|
const [currentThinking, setCurrentThinking] = createSignal('');
|
||||||
let liveStreamUnsubscribe: (() => void) | null = null;
|
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
|
// Effect to manage live stream subscription when expanded
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|
@ -2460,8 +2479,10 @@ function OverviewTab(props: {
|
||||||
<button
|
<button
|
||||||
class="px-3 py-1.5 text-xs font-medium rounded-lg transition-all bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300 border border-purple-300 dark:border-purple-700 hover:bg-purple-200 dark:hover:bg-purple-900/60 disabled:opacity-50 disabled:cursor-not-allowed"
|
class="px-3 py-1.5 text-xs font-medium rounded-lg transition-all bg-purple-100 dark:bg-purple-900/40 text-purple-700 dark:text-purple-300 border border-purple-300 dark:border-purple-700 hover:bg-purple-200 dark:hover:bg-purple-900/60 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
onClick={() => handleForcePatrol(true)}
|
onClick={() => handleForcePatrol(true)}
|
||||||
disabled={forcePatrolLoading() || patrolStatus()?.running}
|
disabled={forcePatrolLoading() || patrolStatus()?.running || patrolRequiresLicense()}
|
||||||
title={patrolStatus()?.running ? 'Patrol in progress - see table below' : 'Run a patrol check now'}
|
title={patrolRequiresLicense()
|
||||||
|
? 'Pulse Pro required to run AI Patrol'
|
||||||
|
: (patrolStatus()?.running ? 'Patrol in progress - see table below' : 'Run a patrol check now')}
|
||||||
>
|
>
|
||||||
<span class="flex items-center gap-1.5">
|
<span class="flex items-center gap-1.5">
|
||||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|
@ -2473,6 +2494,30 @@ function OverviewTab(props: {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show when={patrolRequiresLicense()}>
|
||||||
|
<div class="mb-4 rounded-lg border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 p-3">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<svg class="w-4 h-4 text-amber-600 dark:text-amber-300 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 11V7a4 4 0 00-8 0v4m12 0a2 2 0 012 2v6a2 2 0 01-2 2H8a2 2 0 01-2-2v-6a2 2 0 012-2h8z" />
|
||||||
|
</svg>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium text-amber-800 dark:text-amber-200">Pulse Pro required</p>
|
||||||
|
<p class="text-xs text-amber-700 dark:text-amber-300 mt-1">
|
||||||
|
{patrolLicenseNote() || 'AI Patrol insights require Pulse Pro.'}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
class="inline-flex items-center gap-1 mt-2 text-xs font-medium text-amber-800 dark:text-amber-200 hover:underline"
|
||||||
|
href={patrolUpgradeURL()}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Upgrade to Pulse Pro
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* Summary Stats Bar */}
|
{/* Summary Stats Bar */}
|
||||||
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-3 mb-4 flex flex-wrap items-center gap-4 text-xs">
|
<div class="bg-gray-50 dark:bg-gray-800/50 rounded-lg p-3 mb-4 flex flex-wrap items-center gap-4 text-xs">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
|
|
@ -2506,34 +2551,35 @@ function OverviewTab(props: {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<Show when={!patrolRequiresLicense()}>
|
||||||
<Show
|
<div class="space-y-2">
|
||||||
when={aiFindings().length > 0}
|
<Show
|
||||||
fallback={
|
when={aiFindings().length > 0}
|
||||||
<div class="text-center py-6 border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
fallback={
|
||||||
<div class="flex justify-center mb-2">
|
<div class="text-center py-6 border border-green-200 dark:border-green-800 bg-green-50 dark:bg-green-900/20 rounded-lg">
|
||||||
<svg class="w-10 h-10 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<div class="flex justify-center mb-2">
|
||||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" />
|
<svg class="w-10 h-10 text-green-500 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4" />
|
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" />
|
||||||
</svg>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm font-medium text-green-700 dark:text-green-400">All Systems Healthy</p>
|
||||||
|
<p class="text-xs text-green-600 dark:text-green-500 mt-1">AI patrol found no issues to report</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm font-medium text-green-700 dark:text-green-400">All Systems Healthy</p>
|
}
|
||||||
<p class="text-xs text-green-600 dark:text-green-500 mt-1">AI patrol found no issues to report</p>
|
>
|
||||||
</div>
|
<For each={aiFindings().filter(f => !pendingFixFindings().has(f.id))}>
|
||||||
}
|
{(finding) => {
|
||||||
>
|
const colors = severityColors[finding.severity];
|
||||||
<For each={aiFindings().filter(f => !pendingFixFindings().has(f.id))}>
|
const [isExpanded, setIsExpanded] = createSignal(false);
|
||||||
{(finding) => {
|
return (
|
||||||
const colors = severityColors[finding.severity];
|
<div
|
||||||
const [isExpanded, setIsExpanded] = createSignal(false);
|
class="border rounded-lg transition-all"
|
||||||
return (
|
style={{
|
||||||
<div
|
'background-color': colors.bg,
|
||||||
class="border rounded-lg transition-all"
|
'border-color': colors.border,
|
||||||
style={{
|
}}
|
||||||
'background-color': colors.bg,
|
>
|
||||||
'border-color': colors.border,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Compact header - always visible, clickable */}
|
{/* Compact header - always visible, clickable */}
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-3 p-3 cursor-pointer hover:opacity-80"
|
class="flex items-center gap-3 p-3 cursor-pointer hover:opacity-80"
|
||||||
|
|
@ -2767,36 +2813,38 @@ function OverviewTab(props: {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Suppression Rules - What's being ignored */}
|
|
||||||
<div class="mt-6">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<button
|
|
||||||
class="flex items-center gap-2 text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
|
|
||||||
onClick={() => setShowSuppressionRules(!showSuppressionRules())}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
class={`w-4 h-4 transition-transform ${showSuppressionRules() ? 'rotate-90' : ''}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
🔇 Suppression Rules ({suppressionRules().length} active)
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
|
||||||
onClick={() => setShowAddRuleForm(!showAddRuleForm())}
|
|
||||||
>
|
|
||||||
+ Add Rule
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={!patrolRequiresLicense()}>
|
||||||
|
{/* Suppression Rules - What's being ignored */}
|
||||||
|
<div class="mt-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<button
|
||||||
|
class="flex items-center gap-2 text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
|
||||||
|
onClick={() => setShowSuppressionRules(!showSuppressionRules())}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class={`w-4 h-4 transition-transform ${showSuppressionRules() ? 'rotate-90' : ''}`}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
🔇 Suppression Rules ({suppressionRules().length} active)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
onClick={() => setShowAddRuleForm(!showAddRuleForm())}
|
||||||
|
>
|
||||||
|
+ Add Rule
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Add rule form */}
|
{/* Add rule form */}
|
||||||
<Show when={showAddRuleForm()}>
|
<Show when={showAddRuleForm()}>
|
||||||
|
|
@ -2921,29 +2969,31 @@ function OverviewTab(props: {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
{/* Patrol Check History */}
|
<Show when={!patrolRequiresLicense()}>
|
||||||
<div class="mt-6">
|
{/* Patrol Check History */}
|
||||||
<div class="flex items-center justify-between">
|
<div class="mt-6">
|
||||||
<button
|
<div class="flex items-center justify-between">
|
||||||
class="flex items-center gap-2 text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
|
<button
|
||||||
onClick={() => setShowRunHistory(!showRunHistory())}
|
class="flex items-center gap-2 text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors"
|
||||||
>
|
onClick={() => setShowRunHistory(!showRunHistory())}
|
||||||
<svg
|
|
||||||
class={`w-4 h-4 transition-transform ${showRunHistory() ? 'rotate-90' : ''}`}
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
<svg
|
||||||
</svg>
|
class={`w-4 h-4 transition-transform ${showRunHistory() ? 'rotate-90' : ''}`}
|
||||||
Patrol Check History ({filteredPatrolHistory().length} runs)
|
fill="none"
|
||||||
<Show when={patrolRunHistory().length > 0 && patrolRunHistory()[0].status !== 'healthy'}>
|
stroke="currentColor"
|
||||||
<span class="ml-1 px-1.5 py-0.5 text-[10px] bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300 rounded">Issues Found</span>
|
viewBox="0 0 24 24"
|
||||||
</Show>
|
>
|
||||||
</button>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
Patrol Check History ({filteredPatrolHistory().length} runs)
|
||||||
|
<Show when={patrolRunHistory().length > 0 && patrolRunHistory()[0].status !== 'healthy'}>
|
||||||
|
<span class="ml-1 px-1.5 py-0.5 text-[10px] bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300 rounded">Issues Found</span>
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Next Patrol Timer */}
|
{/* Next Patrol Timer */}
|
||||||
<Show when={nextPatrolIn()}>
|
<Show when={nextPatrolIn()}>
|
||||||
|
|
@ -3303,8 +3353,9 @@ function OverviewTab(props: {
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show >
|
</Show >
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/ai/cost"
|
"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/ai/providers"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
"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/rcourtman/pulse-go-rewrite/internal/utils"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
@ -136,6 +137,11 @@ func (h *AISettingsHandler) GetAlertTriggeredAnalyzer() *ai.AlertTriggeredAnalyz
|
||||||
return h.aiService.GetAlertTriggeredAnalyzer()
|
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
|
// AISettingsResponse is returned by GET /api/settings/ai
|
||||||
// API keys are masked for security
|
// API keys are masked for security
|
||||||
type AISettingsResponse struct {
|
type AISettingsResponse struct {
|
||||||
|
|
@ -1957,7 +1963,11 @@ type PatrolStatusResponse struct {
|
||||||
FindingsCount int `json:"findings_count"`
|
FindingsCount int `json:"findings_count"`
|
||||||
Healthy bool `json:"healthy"`
|
Healthy bool `json:"healthy"`
|
||||||
IntervalMs int64 `json:"interval_ms"` // Patrol interval in milliseconds
|
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"`
|
Critical int `json:"critical"`
|
||||||
Warning int `json:"warning"`
|
Warning int `json:"warning"`
|
||||||
Watch int `json:"watch"`
|
Watch int `json:"watch"`
|
||||||
|
|
@ -1989,6 +1999,12 @@ func (h *AISettingsHandler) HandleGetPatrolStatus(w http.ResponseWriter, r *http
|
||||||
status := patrol.GetStatus()
|
status := patrol.GetStatus()
|
||||||
summary := patrol.GetFindingsSummary()
|
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{
|
response := PatrolStatusResponse{
|
||||||
Running: status.Running,
|
Running: status.Running,
|
||||||
Enabled: h.aiService.IsEnabled(),
|
Enabled: h.aiService.IsEnabled(),
|
||||||
|
|
@ -1999,6 +2015,11 @@ func (h *AISettingsHandler) HandleGetPatrolStatus(w http.ResponseWriter, r *http
|
||||||
FindingsCount: status.FindingsCount,
|
FindingsCount: status.FindingsCount,
|
||||||
Healthy: status.Healthy,
|
Healthy: status.Healthy,
|
||||||
IntervalMs: status.IntervalMs,
|
IntervalMs: status.IntervalMs,
|
||||||
|
LicenseRequired: !hasPatrolFeature,
|
||||||
|
LicenseStatus: licenseStatus,
|
||||||
|
}
|
||||||
|
if !hasPatrolFeature {
|
||||||
|
response.UpgradeURL = "https://pulsemonitor.app/pro"
|
||||||
}
|
}
|
||||||
response.Summary.Critical = summary.Critical
|
response.Summary.Critical = summary.Critical
|
||||||
response.Summary.Warning = summary.Warning
|
response.Summary.Warning = summary.Warning
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue