diff --git a/docs/release-control/v6/internal/records/protection-posture-attention-queue-strong-version-2026-06-25.md b/docs/release-control/v6/internal/records/protection-posture-attention-queue-strong-version-2026-06-25.md index 4eb3b556f..5eee484ca 100644 --- a/docs/release-control/v6/internal/records/protection-posture-attention-queue-strong-version-2026-06-25.md +++ b/docs/release-control/v6/internal/records/protection-posture-attention-queue-strong-version-2026-06-25.md @@ -133,6 +133,11 @@ loop, and routes the operator from real findings/evidence to safe action. evidence, next step, and verification state from existing finding, approval, and workflow fields. The scaffold stays attached to the issue row and does not create a separate proof, trust, or status strip. +- 2026-06-30: Added Proxmox overview monitor-context Patrol coverage posture. + It appears only when Patrol has no current work, failed/latest check, + running check, setup failure, overdue schedule, or pending approval; uses + monitor-context labels; and keeps the Patrol empty-work `Patrol protection + posture` list inside Patrol. ## Evidence @@ -150,8 +155,8 @@ loop, and routes the operator from real findings/evidence to safe action. - Cross-source current-work grouping remains useful, but it belongs inside Patrol or contextual monitoring surfaces rather than a renamed top-level destination. -- Calm-day posture now has a Patrol-side empty-work shape; monitoring-context - posture still needs a separate contextual pass before any monitor page should - show protection or coverage summaries. +- Proxmox now has a monitor-context coverage posture. Additional monitor + surfaces should reuse the same Patrol gating and monitor-label boundary + before showing coverage or protection summaries. - The status coverage gap `protection-posture-attention-queue` tracks the remaining product-surface work. diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 34bb46f68..7ff959677 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -1930,6 +1930,12 @@ a new API state machine, queue contract, or verification-accounting field. and the Patrol verification summary derived from run history, so the page also states whether recent Patrol evidence came from a successful full patrol or only from scoped/erroring runs instead of leaving verification scope implicit and the same-day activity-mix explanation derived from that governed run history, so when a recent full patrol is followed by alert-triggered or anomaly-triggered scoped work the verification surface can explain the mix directly instead of reconstructing it from page-local timing heuristics and the Patrol status recency split, so `last_patrol_at` remains reserved for completed full Patrol sweeps while scoped runs and verification checks advance `last_activity_at` without claiming a fresh full-estate verification pass + and the monitor-context Patrol coverage posture boundary, so specific + monitor overviews may consume API-owned Patrol status, latest run history, + current findings, and pending approvals as semantic input for a compact + coverage/schedule signal, but they must not add a new payload, infer + resource-specific proof from global Patrol counters, or duplicate the Patrol + queue/empty-work contract on generic dashboard or Home surfaces and the Patrol Assistant handoff model, so frontend handoff prompts pass current finding context, safe action posture, and resource references as bounded request metadata while leaving tool selection and remediation diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 597eab2c3..13f9886a1 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -1391,7 +1391,11 @@ settings`, and candidate review when discovered sources are waiting. It must context only after current findings, approvals, failed checks, running checks, setup failures, and overdue scheduled protection are ruled out; it must stay subordinate to the empty queue and must not become a generic Home, - dashboard, trust, or proof strip. The + dashboard, trust, or proof strip. Monitor-context Patrol coverage posture + may use the same compact list and badge primitives on a specific platform + overview when it is attached to that monitor surface, but it must use + distinct monitor labels and must not become a nested card, generic + dashboard strip, trust summary, or duplicate Patrol empty-work list. The Patrol schedule and model drawer is part of that shared feature-presentation boundary: it must stay viewport-bounded, expose an accessible dialog label, keep the four-level control policy on the default diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 3ce1b46b7..e709d247d 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -102,6 +102,13 @@ or historical proof/counting for resolved-only work. history, and finding composition, but it must stay contextual to Patrol's empty queue and must not render on monitor-first launch pages, become a Home summary, or promote trust/history counters into current work. + Monitor-context coverage posture is a separate presentation boundary. It may + appear on a specific monitor overview, such as Proxmox, only after the same + current-work, failed-check, setup, running-check, and overdue-schedule gates + have ruled out Patrol work. It must use monitor-context labels, point active + work back to Patrol, omit drift/trust proof counters, and must not reuse the + `Patrol protection posture` list as a launch-page strip or generic Home + summary. Runtime/setup findings are still active Patrol work, but setup-only runtime failures must read as one setup task rather than an infrastructure issue queue. The header may suppress run and schedule/model controls while setup is diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 3e62edc5f..b72c492a0 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -110,6 +110,13 @@ interpreted as backup visibility, recovery readiness, restore capability, or a storage/recovery-owned command path. Suppressing those notices when the API has no deployable agent update target is likewise lifecycle/frontend behavior and does not change Proxmox backup or recovery evidence. +Proxmox overview Patrol coverage posture is also adjacent monitor/Patrol +context, not storage/recovery evidence. `ProxmoxPageSurface` may render a +compact monitor-context Patrol coverage list from Patrol status, latest run, +current findings, and approvals when no current Patrol work or failed/stale +Patrol check exists, but that list must not be treated as backup coverage, +restore readiness, PBS verification/protection proof, or a replacement for the +Proxmox Backups tab and workload Backup column. Proxmox backup inventory loading and load-failure chrome is likewise a frontend-primitives dependency. `ProxmoxBackupsTable` owns the backup API queries, recovery model, filters, coverage split, and backup-specific error diff --git a/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.test.ts b/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.test.ts index 076e07d82..a061e5e8e 100644 --- a/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/patrolControlPresentation.test.ts @@ -5,6 +5,7 @@ import { getPatrolProInvestigationHandoff, getPatrolQueueBadgeLabel, getPatrolQueueWorkspaceDescription, + getMonitorContextPatrolProtectionPosture, getPatrolReadyWorkDetail, getPatrolSetupIssueReason, getPatrolWorkspaceProtectionPosture, @@ -418,6 +419,103 @@ describe('patrolControlPresentation', () => { ).toEqual([]); }); + it('surfaces monitor-context posture without reusing the Patrol empty-work labels', () => { + expect( + getMonitorContextPatrolProtectionPosture({ + findingCount: 0, + latestRun: { + error_count: 0, + resources_checked: 4, + status: 'healthy', + }, + monitoredResourceCount: 4, + nowMs: Date.parse('2026-06-30T13:00:00Z'), + patrolStatus: { + enabled: true, + error_count: 0, + findings_count: 0, + healthy: true, + next_patrol_at: '2026-06-30T14:05:00Z', + resources_checked: 4, + running: false, + runtime_state: 'active', + }, + pendingApprovalCount: 0, + workTypeComposition: { + total: 0, + approval: 0, + failed: 0, + inProgress: 0, + recurring: 0, + newIssues: 0, + }, + }), + ).toEqual([ + { + detail: 'Latest Patrol evidence is available while you review this monitor view.', + id: 'coverage', + label: 'Patrol checked 4 resources', + tone: 'success', + }, + { + detail: 'Current Patrol findings and approvals stay in Patrol; none are waiting now.', + id: 'open-work', + label: 'No Patrol work waiting', + tone: 'success', + }, + { + detail: 'Patrol is scheduled to check monitored resources again.', + id: 'schedule', + label: 'Next check scheduled', + tone: 'info', + }, + ]); + }); + + it('does not surface monitor-context posture without monitor resources or when Patrol has work', () => { + const baseInput = { + latestRun: { + error_count: 0, + resources_checked: 4, + status: 'healthy', + }, + nowMs: Date.parse('2026-06-30T13:00:00Z'), + patrolStatus: { + enabled: true, + error_count: 0, + findings_count: 0, + healthy: true, + next_patrol_at: '2026-06-30T14:05:00Z', + resources_checked: 4, + running: false, + runtime_state: 'active', + }, + } as const; + + expect( + getMonitorContextPatrolProtectionPosture({ + ...baseInput, + monitoredResourceCount: 0, + }), + ).toEqual([]); + + expect( + getMonitorContextPatrolProtectionPosture({ + ...baseInput, + findingCount: 1, + monitoredResourceCount: 4, + }), + ).toEqual([]); + + expect( + getMonitorContextPatrolProtectionPosture({ + ...baseInput, + monitoredResourceCount: 4, + pendingApprovalCount: 1, + }), + ).toEqual([]); + }); + it('keeps setup-only issue reasons short and actionable', () => { expect( getPatrolSetupIssueReason({ diff --git a/frontend-modern/src/features/patrol/patrolControlPresentation.ts b/frontend-modern/src/features/patrol/patrolControlPresentation.ts index c712e9102..44528b1df 100644 --- a/frontend-modern/src/features/patrol/patrolControlPresentation.ts +++ b/frontend-modern/src/features/patrol/patrolControlPresentation.ts @@ -73,6 +73,13 @@ export interface PatrolWorkspaceProtectionPostureSummary { tone: MetadataBadgeTone; } +export interface MonitorContextPatrolPostureSummary { + detail: string; + id: 'coverage' | 'open-work' | 'schedule'; + label: string; + tone: MetadataBadgeTone; +} + interface PatrolWorkspaceWorkGroupsInput { latestRun?: Pick | null; nowMs?: number; @@ -108,6 +115,10 @@ interface PatrolWorkspaceProtectionPostureInput { workTypeComposition?: PatrolWorkTypeComposition; } +interface MonitorContextPatrolPostureInput extends PatrolWorkspaceProtectionPostureInput { + monitoredResourceCount?: number; +} + interface PatrolSetupIssueReasonInput { setupFindingTitle?: string; readinessSummary?: string; @@ -180,6 +191,11 @@ function getPatrolCoverageLabel( return undefined; } +function formatMonitorCoverageLabel(coverageLabel: string | undefined): string { + if (!coverageLabel) return 'Patrol coverage needs refresh'; + return `Patrol ${coverageLabel.charAt(0).toLowerCase()}${coverageLabel.slice(1)}`; +} + function getPatrolQueueActionDetail(input: PatrolControlCopyInput): string { if (input.autonomyLocked) { return 'Open a row to review evidence and record the outcome.'; @@ -456,6 +472,64 @@ export function getPatrolWorkspaceProtectionPosture( return posture; } +export function getMonitorContextPatrolProtectionPosture( + input: MonitorContextPatrolPostureInput, +): MonitorContextPatrolPostureSummary[] { + if (normalizeCount(input.monitoredResourceCount) <= 0) { + return []; + } + + const patrolPosture = getPatrolWorkspaceProtectionPosture(input); + if (patrolPosture.length === 0) { + return []; + } + + const patrolStatus = input.patrolStatus ?? null; + const healthyTone: MetadataBadgeTone = patrolStatus?.healthy === false ? 'warning' : 'success'; + const coverageLabel = getPatrolCoverageLabel(input.latestRun ?? null, patrolStatus); + const summaries: MonitorContextPatrolPostureSummary[] = [ + { + id: 'coverage', + label: formatMonitorCoverageLabel(coverageLabel), + detail: coverageLabel + ? 'Latest Patrol evidence is available while you review this monitor view.' + : 'Run Patrol to refresh current coverage for monitored resources.', + tone: coverageLabel ? healthyTone : 'warning', + }, + { + id: 'open-work', + label: 'No Patrol work waiting', + detail: 'Current Patrol findings and approvals stay in Patrol; none are waiting now.', + tone: healthyTone, + }, + ]; + + if (patrolStatus?.enabled === false) { + summaries.push({ + id: 'schedule', + label: 'Scheduled checks paused', + detail: 'Run Patrol manually or enable scheduled checks to keep coverage fresh.', + tone: 'warning', + }); + } else if (patrolStatus?.next_patrol_at) { + summaries.push({ + id: 'schedule', + label: 'Next check scheduled', + detail: 'Patrol is scheduled to check monitored resources again.', + tone: 'info', + }); + } else { + summaries.push({ + id: 'schedule', + label: 'Ready to run Patrol', + detail: 'Run Patrol from the Patrol page any time to refresh coverage.', + tone: 'info', + }); + } + + return summaries; +} + const PATROL_PRO_HANDOFF_ACTIONABLE_SEVERITIES = new Set(['critical', 'warning']); export interface PatrolProInvestigationHandoff { diff --git a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx index 18f3488f0..6be618848 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx @@ -1,6 +1,7 @@ import { useLocation } from '@solidjs/router'; -import { Show, createMemo, createResource, type Accessor } from 'solid-js'; +import { For, Show, createMemo, createResource, type Accessor } from 'solid-js'; import { StatusDot } from '@/components/shared/StatusDot'; +import { MetadataBadge } from '@/components/shared/MetadataBadge'; import StorageSurface from '@/components/Storage/Storage'; import { WorkloadsFilter } from '@/components/Workloads/WorkloadsFilter'; import { WorkloadsSurface } from '@/components/Workloads/WorkloadsSurface'; @@ -42,6 +43,15 @@ import { ProxmoxNodesTable } from './ProxmoxNodesTable'; import { ProxmoxReplicationTable, fetchReplicationJobs } from './ProxmoxReplicationTable'; import { useUnifiedResources } from '@/hooks/useUnifiedResources'; import { updateStore } from '@/stores/updates'; +import { aiIntelligenceStore } from '@/stores/aiIntelligence'; +import { getPatrolWorkTypeComposition } from '@/utils/aiFindingPresentation'; +import { + getPatrolRunHistory, + getPatrolStatus, + type PatrolRunRecord, + type PatrolStatus, +} from '@/api/patrol'; +import { getMonitorContextPatrolProtectionPosture } from '@/features/patrol/patrolControlPresentation'; import { PROXMOX_TAB_SPECS, buildProxmoxPageModel, @@ -75,6 +85,25 @@ const PROXMOX_WORKLOAD_STATUS_OPTIONS: readonly WorkloadsStatusOption[] = [ const ProxmoxIcon = getPlatformIcon('proxmox'); const proxmoxIcon = () => ; +interface MonitorPatrolContext { + latestRun: PatrolRunRecord | null; + status: PatrolStatus | null; +} + +async function loadMonitorPatrolContext(): Promise { + const [, , statusResult, historyResult] = await Promise.allSettled([ + aiIntelligenceStore.loadPatrolFindings(), + aiIntelligenceStore.loadPendingApprovals(), + getPatrolStatus(), + getPatrolRunHistory(1), + ]); + + return { + status: statusResult.status === 'fulfilled' ? statusResult.value : null, + latestRun: historyResult.status === 'fulfilled' ? (historyResult.value[0] ?? null) : null, + }; +} + export function ProxmoxPageSurface() { const location = useLocation(); const { resources, loading, error, refetch } = useUnifiedResources({ @@ -89,6 +118,7 @@ export function ProxmoxPageSurface() { // Reading an errored resource throws, hence the `.error` guards. const [replicationJobs, { refetch: refetchReplicationJobs }] = createResource(fetchReplicationJobs); + const [monitorPatrolContext] = createResource(loadMonitorPatrolContext); const replicationJobCount = createMemo(() => replicationJobs.error ? 0 : (replicationJobs() ?? []).length, ); @@ -196,6 +226,7 @@ export function ProxmoxPageSurface() { setMetricDisplayMode={setMetricDisplayMode} metricHistoryRange={metricHistoryRange} setMetricHistoryRange={setMetricHistoryRange} + patrolContext={monitorPatrolContext} /> @@ -252,6 +283,7 @@ interface ProxmoxOverviewProps { setMetricDisplayMode: (value: WorkloadsMetricDisplayMode) => void; metricHistoryRange: Accessor; setMetricHistoryRange: (value: WorkloadTableMetricHistoryRange) => void; + patrolContext: Accessor; } function ProxmoxOverview(props: ProxmoxOverviewProps) { @@ -288,6 +320,19 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) { workloadsState.search(), ), ); + const activePatrolFindings = createMemo(() => + aiIntelligenceStore.patrolFindings.filter((finding) => finding.status === 'active'), + ); + const monitorPatrolPosture = createMemo(() => + getMonitorContextPatrolProtectionPosture({ + findingCount: activePatrolFindings().length, + latestRun: props.patrolContext()?.latestRun ?? null, + monitoredResourceCount: props.model().resources.length, + patrolStatus: props.patrolContext()?.status ?? null, + pendingApprovalCount: aiIntelligenceStore.patrolPendingApprovalCount, + workTypeComposition: getPatrolWorkTypeComposition(activePatrolFindings()), + }), + ); return (
@@ -329,6 +374,29 @@ function ProxmoxOverview(props: ProxmoxOverviewProps) { />
+ 0}> +
+ + {(item) => ( +
+
+ + {item.label} + +
+

{item.detail}

+
+ )} +
+
+
{ expect(proxmoxPageSurfaceSource).toContain('workloads={model().guests}'); expect(proxmoxPageSurfaceSource).not.toContain('workloads={workloadsState.allGuests'); }); + + it('keeps monitor-context Patrol coverage out of backup evidence surfaces', () => { + expect(proxmoxPageSurfaceSource).toContain('getMonitorContextPatrolProtectionPosture'); + expect(proxmoxPageSurfaceSource).toContain('getPatrolRunHistory(1)'); + expect(proxmoxPageSurfaceSource).toContain('aria-label="Proxmox Patrol coverage"'); + expect(proxmoxPageSurfaceSource).not.toContain('aria-label="Patrol protection posture"'); + expect(proxmoxBackupsTableSource).not.toContain('Proxmox Patrol coverage'); + }); }); diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx index abe29b247..8ab8564d3 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.test.tsx @@ -1,4 +1,4 @@ -import { cleanup, render, screen } from '@solidjs/testing-library'; +import { cleanup, render, screen, waitFor } from '@solidjs/testing-library'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { Resource } from '@/types/resource'; import { ProxmoxPageSurface } from '../ProxmoxPageSurface'; @@ -6,6 +6,20 @@ import { ProxmoxPageSurface } from '../ProxmoxPageSurface'; const mockUseUnifiedResources = vi.fn(); const mockPathname = vi.hoisted(() => vi.fn(() => '/proxmox/overview')); const mockVersionInfo = vi.hoisted(() => vi.fn()); +const mockGetPatrolStatus = vi.hoisted(() => vi.fn()); +const mockGetPatrolRunHistory = vi.hoisted(() => vi.fn()); +const mockLoadPatrolFindings = vi.hoisted(() => vi.fn()); +const mockLoadPendingApprovals = vi.hoisted(() => vi.fn()); +const mockPatrolState = vi.hoisted(() => ({ + findings: [] as Array<{ + investigationOutcome?: string; + investigationStatus?: string; + regressionCount?: number; + status: string; + timesRaised?: number; + }>, + pendingApprovalCount: 0, +})); const makeResource = (resource: Partial & Pick): Resource => ({ @@ -43,6 +57,24 @@ vi.mock('@/stores/updates', () => ({ }, })); +vi.mock('@/stores/aiIntelligence', () => ({ + aiIntelligenceStore: { + get patrolFindings() { + return mockPatrolState.findings; + }, + get patrolPendingApprovalCount() { + return mockPatrolState.pendingApprovalCount; + }, + loadPatrolFindings: mockLoadPatrolFindings, + loadPendingApprovals: mockLoadPendingApprovals, + }, +})); + +vi.mock('@/api/patrol', () => ({ + getPatrolStatus: mockGetPatrolStatus, + getPatrolRunHistory: mockGetPatrolRunHistory, +})); + vi.mock('@solidjs/router', async () => { const actual = await vi.importActual('@solidjs/router'); return { @@ -116,6 +148,27 @@ describe('ProxmoxPageSurface contract', () => { beforeEach(() => { mockPathname.mockReturnValue('/proxmox/overview'); mockVersionInfo.mockReturnValue(null); + mockPatrolState.findings = []; + mockPatrolState.pendingApprovalCount = 0; + mockLoadPatrolFindings.mockResolvedValue(undefined); + mockLoadPendingApprovals.mockResolvedValue(undefined); + mockGetPatrolStatus.mockResolvedValue({ + enabled: true, + error_count: 0, + findings_count: 0, + healthy: true, + next_patrol_at: '2099-06-30T14:05:00Z', + resources_checked: 4, + running: false, + runtime_state: 'active', + }); + mockGetPatrolRunHistory.mockResolvedValue([ + { + error_count: 0, + resources_checked: 4, + status: 'healthy', + }, + ]); }); afterEach(() => { @@ -189,6 +242,47 @@ describe('ProxmoxPageSurface contract', () => { expect(totals).toHaveTextContent('1 stopped'); }); + it('renders monitor-context Patrol coverage without using the Patrol empty-work strip', async () => { + setResources([ + makeResource({ + id: 'agent:pve-1', + type: 'agent', + proxmox: { nodeName: 'pve-1', clusterName: 'homelab' }, + }), + ]); + + render(() => ); + + await waitFor(() => expect(mockGetPatrolStatus).toHaveBeenCalled()); + const posture = await screen.findByRole('list', { name: 'Proxmox Patrol coverage' }); + expect(mockLoadPatrolFindings).toHaveBeenCalled(); + expect(mockLoadPendingApprovals).toHaveBeenCalled(); + expect(posture).toHaveTextContent('Patrol checked 4 resources'); + expect(posture).toHaveTextContent('No Patrol work waiting'); + expect(posture).toHaveTextContent('Next check scheduled'); + expect(screen.queryByText('Protection current')).not.toBeInTheDocument(); + expect( + screen.queryByRole('list', { name: 'Patrol protection posture' }), + ).not.toBeInTheDocument(); + }); + + it('does not render monitor-context Patrol coverage when active Patrol work exists', () => { + mockPatrolState.findings = [{ status: 'active' }]; + setResources([ + makeResource({ + id: 'agent:pve-1', + type: 'agent', + proxmox: { nodeName: 'pve-1', clusterName: 'homelab' }, + }), + ]); + + render(() => ); + + expect( + screen.queryByRole('list', { name: 'Proxmox Patrol coverage' }), + ).not.toBeInTheDocument(); + }); + it('does not surface stale-agent notices for development builds without an agent target', () => { mockVersionInfo.mockReturnValue({ version: '6.0.0-rc.6+git.172.g2c360f779.dirty', diff --git a/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts b/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts index ea858bb64..c566d283f 100644 --- a/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts +++ b/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts @@ -503,12 +503,15 @@ test.describe("Monitor-first Patrol workbench browser contract", () => { await expect(page.getByRole("heading", { name: "Open work" })).toHaveCount( 0, ); + await expect( + page.getByRole("list", { name: "Proxmox Patrol coverage" }), + ).toHaveCount(0); await expect( page.getByRole("heading", { name: /^Pulse Assistant$/ }), ).toHaveCount(0); }); - test("calm-day Patrol posture stays inside Patrol context after infrastructure launch", async ({ + test("calm-day Patrol coverage stays contextual after infrastructure launch", async ({ page, }, testInfo) => { test.skip( @@ -533,6 +536,19 @@ test.describe("Monitor-first Patrol workbench browser contract", () => { await expect( page.getByRole("list", { name: "Patrol protection posture" }), ).toHaveCount(0); + const monitorPatrolCoverage = page.getByRole("list", { + name: "Proxmox Patrol coverage", + }); + await expect(monitorPatrolCoverage).toBeVisible(); + await expect( + monitorPatrolCoverage.getByText("Patrol checked 1 resource"), + ).toBeVisible(); + await expect( + monitorPatrolCoverage.getByText("No Patrol work waiting"), + ).toBeVisible(); + await expect( + monitorPatrolCoverage.getByText("Next check scheduled"), + ).toBeVisible(); await page.getByRole("tab", { name: "Patrol" }).click(); await expect(page).toHaveURL(/\/patrol$/); @@ -580,6 +596,9 @@ test.describe("Monitor-first Patrol workbench browser contract", () => { await expect(page.getByText("Latest check needs review")).toHaveCount(0); await expect(page.getByText("1 approval waiting")).toHaveCount(0); await expect(page.getByText("Protection current")).toHaveCount(0); + await expect( + page.getByRole("list", { name: "Proxmox Patrol coverage" }), + ).toHaveCount(0); await expect(page.getByText("What Pulse checked")).toHaveCount(0); await page.getByRole("tab", { name: /Patrol/ }).click();