patrol settings: describe Watch only for plan-locked installs

The Settings > Patrol "Patrol mode" section told every install to choose a mode
on the Patrol page, but plan-locked (free) installs have no mode choice there —
Watch only is their only mode and the paid modes stay off the free surface. The
copy was tier-blind and sent free users on a fool's errand.

Make the section tier-aware via the canonical patrol autonomy availability
resolver: plan-locked installs now read "This install runs Watch only. Patrol
monitors your infrastructure and reports issues here." while Pro/runtime installs
keep the choose-on-the-Patrol-page redirect. The Open Patrol button stays.
This commit is contained in:
rcourtman
2026-06-24 21:26:37 +01:00
parent 6b610ebed9
commit 68ede71fd8
2 changed files with 48 additions and 3 deletions
@@ -1,4 +1,4 @@
import { Component, Show } from 'solid-js';
import { Component, createMemo, Show } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { AIChatMaintenanceSection } from '@/components/Settings/AIChatMaintenanceSection';
import { AISettingsDialogs } from '@/components/Settings/AISettingsDialogs';
@@ -19,6 +19,13 @@ import { LoadingSpinner } from '@/components/shared/LoadingSpinner';
import SettingsPanel from '@/components/shared/SettingsPanel';
import { Toggle } from '@/components/shared/Toggle';
import { PATROL_PATH } from '@/routing/resourceLinks';
import { getPatrolAutonomyAvailabilityPresentation } from '@/features/patrol/patrolAutonomyAvailability';
import { getRuntimeCapabilityBlock, hasFeature, runtimeCapabilities } from '@/stores/license';
import { getUpgradeActionDestination } from '@/stores/licenseCommercial';
import {
presentationPolicyHidesCommercialSurfaces,
presentationPolicyHidesUpgradePrompts,
} from '@/stores/sessionPresentationPolicy';
import {
AI_SETTINGS_PANEL_TITLE,
getAISettingsLoadingState,
@@ -93,6 +100,17 @@ const PatrolSettingsContent: Component<{ state: ReturnType<typeof useAISettingsS
props,
) => {
const navigate = useNavigate();
const patrolModeAvailability = createMemo(() =>
getPatrolAutonomyAvailabilityPresentation({
autoFixLocked: !hasFeature('ai_autofix'),
commercialSurfacesHidden: presentationPolicyHidesCommercialSurfaces(),
upgradePromptsHidden: presentationPolicyHidesUpgradePrompts(),
runtimeCapabilityBlock: getRuntimeCapabilityBlock('ai_autofix'),
runtime: runtimeCapabilities()?.runtime,
planUpgradeDestination: getUpgradeActionDestination('ai_autofix'),
}),
);
const isPlanLockedPatrol = createMemo(() => patrolModeAvailability().kind === 'plan_locked');
const intervalOptions = [
{ value: 0, label: 'Manual only' },
{ value: 60, label: 'Every hour' },
@@ -109,8 +127,13 @@ const PatrolSettingsContent: Component<{ state: ReturnType<typeof useAISettingsS
<div>
<h3 class="text-sm font-semibold text-base-content">Patrol mode</h3>
<p class="mt-1 text-xs text-muted">
Choose a Patrol mode on the Patrol page. Keep schedule, triggers, and model readiness
here.
<Show
when={isPlanLockedPatrol()}
fallback="Choose a Patrol mode on the Patrol page. Keep schedule, triggers, and model readiness here."
>
This install runs Watch only. Patrol monitors your infrastructure and reports issues
here.
</Show>
</p>
</div>
<button
@@ -82,7 +82,9 @@ vi.mock('@/utils/logger', () => ({
vi.mock('@/stores/license', () => ({
hasFeature: (...args: unknown[]) => hasFeatureMock(...args),
getRuntimeCapabilityBlock: () => undefined,
loadRuntimeCapabilities: (...args: unknown[]) => loadLicenseStatusMock(...args),
runtimeCapabilities: () => ({ capabilities: [], runtime: undefined }),
}));
vi.mock('@/stores/licenseCommercial', () => ({
@@ -95,6 +97,7 @@ vi.mock('@/stores/licenseCommercial', () => ({
}));
vi.mock('@/stores/sessionPresentationPolicy', () => ({
presentationPolicyHidesCommercialSurfaces: () => false,
presentationPolicyHidesUpgradePrompts: () => presentationPolicyHidesUpgradePromptsMock(),
}));
@@ -295,6 +298,25 @@ describe('AISettings model loading error states', () => {
expect(screen.queryByText('External agents')).not.toBeInTheDocument();
});
it('describes Watch only for plan-locked installs instead of telling the user to choose a mode', async () => {
hasFeatureMock.mockImplementation((feature: string) => feature !== 'ai_autofix');
getSettingsMock.mockResolvedValue({
...baseSettings(),
enabled: true,
configured: true,
patrol_interval_minutes: 180,
});
renderComponent('patrol');
expect(await screen.findByText('Patrol mode')).toBeInTheDocument();
expect(screen.getByText(/This install runs Watch only/i)).toBeInTheDocument();
expect(
screen.queryByText(/Choose a Patrol mode on the Patrol page/i),
).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: /Open Patrol/i })).toBeInTheDocument();
});
it('saves Patrol trigger settings from Pulse Intelligence Patrol settings', async () => {
getSettingsMock.mockResolvedValue({
...baseSettings(),