diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index 69e8478a0..f9d3486d0 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -7467,7 +7467,7 @@ "status": "target-met", "completion": { "state": "complete", - "summary": "Monitor-first Patrol operations now has a decision-first, responsive governed floor: authenticated launch stays on infrastructure monitoring; a compact control toolbar gives way to a daily briefing with current decision, critical, and separately canonical approval posture plus one recommended start; Patrol owns a persistent desktop inbox-and-context workspace and a true mobile master/detail flow with identity-stable review progress, previous/next navigation, automatic advancement after reviewed or temporarily suppressed occurrences, and explicit remaining-work feedback over canonical lifecycle, evidence, protection, availability, approval, action, and verification truth; objective setup and verified receipts remain compact supporting context; calm states stay honest; and Assistant remains contextual explanation for the selected thing rather than the operations front door.", + "summary": "Monitor-first Patrol operations now has a one-task-at-a-time, decision-first governed floor: authenticated launch stays on infrastructure monitoring; a compact secondary status row gives way to mutually exclusive Inbox, Protection, and Activity modes; Inbox defaults to one full-width ranked decision queue without duplicate counters, generic row state, or an empty preview; selection alone creates the persistent desktop inbox-and-context workspace or true mobile master/detail flow with identity-stable review progress, previous/next navigation, automatic advancement after reviewed or temporarily suppressed occurrences, and explicit remaining-work feedback over canonical lifecycle, evidence, protection, availability, approval, action, and verification truth; objectives, verified receipts, approvals, and records remain deliberately accessible without competing on the same canvas; calm states stay honest; and Assistant remains contextual explanation for the selected thing rather than the operations front door.", "tracking": [] }, "blockers": [], diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 0f438a4c2..061821abb 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -2519,6 +2519,12 @@ a new API state machine, queue contract, or verification-accounting field. attention projection by severity, then actionable review posture, then observation recency, but it must preserve every API-owned lifecycle, evidence, protection, action, and verification fact in selected detail. + The first-party `Inbox`, `Protection`, and `Activity` modes are a mutually + exclusive presentation boundary, not API query modes: Inbox consumes the + canonical attention projection, Protection consumes retained objectives, + and Activity consumes verified receipts plus existing action/finding/run + records. Switching panels must not rewrite membership, merge counts, invent + lifecycle state, or refetch one dataset as though it represented another. Secondary receipt responses that are absent or incomplete may degrade to an unavailable or empty supporting panel; they must not unmount the current attention workbench or convert missing receipts into successful work diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index c14553efd..f7fedfb25 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -772,16 +772,17 @@ assessment semantics, but they must keep those semantics in their governed presentation helpers and render them inside the shared neutral Pulse surface language rather than introducing page-local verdict bands or nested cards. The Patrol operator home composes the shared `PageHeader`, `Button`, `Toggle`, -`MetadataBadge`, native disclosure, dialog, and neutral bordered-surface -patterns. Patrol owns the meaning and order of background posture, genuine -decisions, objectives, verified receipts, and secondary operational records; the -shared primitive boundary owns keyboard focus, touch targets, responsive -stacking, and visual consistency. Plan-locked paid-mode discovery is not a +`MetadataBadge`, native disclosure, dialog, tab, and neutral bordered-surface +patterns. Patrol owns the meaning of background posture and the mutually +exclusive `Inbox`, `Protection`, and `Activity` modes; the shared primitive +boundary owns visible selection, Arrow/Home/End keyboard movement, roving tab +focus, touch targets, responsive stacking, and visual consistency. Only the +selected mode's panel is rendered. Plan-locked paid-mode discovery is not a daily-page primitive and must not be added beside the effective mode. -Current API-owned decisions must follow the compact posture header and precede -objective configuration at every viewport; an empty objective state must not -push current work below the first useful screen. The compact `Verified outcomes` -list consumes server-authored Patrol work +Current API-owned decisions must follow the compact posture row in the default +Inbox at every viewport. Objective configuration and verified/history surfaces +belong to their explicit modes rather than following the queue on the same +canvas. The compact `Verified outcomes` list consumes server-authored Patrol work receipts rather than filtering generic history in the browser. It may format the canonical capability name and verification time with shared presentation helpers, and it reuses the action resource presenter for durable resources that diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 979a5a3ac..fd53bc981 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -75,16 +75,16 @@ sources, and retains the note as operator context. ### Quiet operator home The first-party Patrol page is an outcome and exception surface, not a mirror of -the internal lifecycle. Its primary order is the effective background posture, -work that genuinely needs the operator, the outcomes Patrol is looking after, -and concise recent receipts. Active work must outrank objective configuration -on the daily-use route, especially on a narrow viewport; an empty objectives -surface cannot push current decisions below the first useful screen. -Investigation internals, lifecycle filters, raw findings, checks, and run -history remain available as secondary operational records without competing -with those four questions. Action review/audit and finding/run records use one -subordinate `Review and history` handoff area so the page does not present -several competing history concepts. +the internal lifecycle. After the effective background posture it exposes three +mutually exclusive operator modes: `Inbox`, `Protection`, and `Activity`. +`Inbox` is the default and mounts only genuine operator decisions; +`Protection` owns retained objectives; `Activity` owns verified receipts plus +the subordinate action-audit and finding/run-record handoffs. Active decisions, +objective configuration, receipts, and operational records must not all render +at once. Investigation internals, lifecycle filters, raw findings, checks, and +run history remain deliberately available without competing with the daily +decision queue. The mode selector is keyboard navigable and retains the stable +Patrol route rather than inventing API filters or lifecycle states. `patrolHomePresentation.ts` owns the deterministic projection from effective autonomy and typed attention evidence into `needs user` versus `can continue` @@ -1204,15 +1204,16 @@ The durable decision and proof record is ## Current State -The Patrol 2.0 frontend now uses a decision-first information hierarchy. The -compact command header owns enabled posture, effective mode, last/next check -context, manual check, settings, and the secondary mode disclosure. The typed -attention workbench is the first substantial page surface on desktop and -mobile. Protected objectives and proof-constrained verified outcomes follow as -supporting continuity surfaces; empty objectives and empty receipts stay -compact instead of consuming the prime viewport. Universal governed actions -and Patrol-specific finding/run records are exposed together through the -subordinate `Review and history` handoff area. +The Patrol 2.0 frontend now uses a one-task-at-a-time, decision-first +information hierarchy. The compact, borderless command row owns enabled +posture, effective mode, last/next check context, manual check, settings, and +the secondary mode disclosure without presenting a second primary CTA card. +The typed attention workbench is the only substantial default page surface on +desktop and mobile. `Protection` deliberately swaps in retained objectives; +`Activity` swaps in proof-constrained verified outcomes and the subordinate +`Review and history` handoff for universal governed actions and Patrol-specific +finding/run records. Empty continuity surfaces therefore never share the prime +viewport with current decisions. The page introduction states the operator job—find the decision, choose the next step, and retain a verified record—rather than repeating the command @@ -1224,17 +1225,18 @@ does not merge governed action records into the attention list or invent an attention count; the queue and approval inbox retain their separate canonical projections. -The default loaded state is a daily briefing rather than an unframed list. It -states how many current decisions need the operator, shows the current loaded -critical-decision and separately canonical approval counts, explains the queue -ordering, and offers one `Start review` action for the highest-priority item. -On wide layouts the inbox and a recommended first-decision preview occupy a -persistent two-pane workspace before selection; selection replaces the preview -with the full governed context without hiding the inbox. On narrow layouts the -selected context intentionally replaces the list until `Back to list`, making -Patrol a true master/detail workflow instead of stacking the detail above a -second full queue. The command header remains compact so the briefing and first -decision enter the initial viewport. +The default loaded state is a full-width decision inbox with one headline that +states how many current decisions need the operator, one short explanation of +the ordering, one `Start review` action for the highest-priority item, the +canonical pending-approval handoff when needed, and refresh. It must not repeat +the same facts as a metric strip, generic row state, generic `Review` label, or +empty recommended-item preview. A two-pane workspace appears only after the +operator selects or deep-links a decision; wide layouts retain the inbox beside +the full governed context. On narrow layouts the selected context intentionally +replaces the list until `Back to list`, making Patrol a true master/detail +workflow instead of stacking the detail above a second full queue. The command +row remains compact and visually secondary so the inbox and first decisions +enter the initial viewport. Selecting the first item starts a bounded review session over the loaded decision order. The selected context shows its current position and exposes @@ -1249,10 +1251,12 @@ that the occurrence will return at the chosen time. Clearing the final item returns to the calm inbox with an honest completion announcement. The attention queue orders genuine operator decisions by severity, actionable -approval/review state, and latest observation time. Rows expose the severity -word, lifecycle state, bounded consequence, affected resource, decision label, -and freshness without repeating full mode-policy prose on every item. Selecting -an item opens one responsive decision context: the current summary, affected +approval/review state, and latest observation time. Collapsed rows expose the +severity word, bounded consequence, affected resource, and freshness; only +exceptional approval-needed or verification-result work adds another label. +Lifecycle state remains selected-detail truth instead of a repeated `Open` +badge, and ordinary queue membership is not restated as `Review` on every row. +Selecting an item opens one responsive decision context: the current summary, affected resource, impact and recommended next step precede governed action review; occurrence acknowledgement/suppression, evidence, protection posture, timeline, resource navigation, and contextual Assistant explanation remain available diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 7cebe54bc..78caddafd 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,26 +1,22 @@ { "version": 1, - "base_sha": "0da7092991898ca4b125f75cd7dacf9d492b7126", - "verified_at": "2026-08-18T10:05:00Z", + "base_sha": "e1427e66f8614d0fea90d62d7f785f93595fdae1", + "verified_at": "2026-08-18T09:24:31Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/useAppRuntimeState.ts" + "frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx", + "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx", + "frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx" ], "content_sha256": { - "frontend-modern/src/useAppRuntimeState.ts": "311d1b7c6a97b94830311650b4efe5ee5cf57215c903e9835be70441a62836dd" + "frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "770c7eaecf23026916fa48198fe5220be6db076fbd30a7351e250d2cbeec9fb4", + "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx": "f147df3ec75a27fb5d06b6cf9e8f487b02d93af1630c8d7fb0fd3688b264a3dc", + "frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx": "4a3d400d59c42a48b0488ef0a4b0026f39326e1cbfc44d19528198bfd84030e1" }, - "routes": [ - "/login", - "/", - "/proxmox/overview", - "/docker/overview", - "/kubernetes/overview", - "/truenas/overview", - "/vsphere/overview" - ], + "routes": ["/patrol"], "viewports": [ { - "width": 1440, + "width": 1280, "height": 900 }, { @@ -29,25 +25,18 @@ } ], "states": [ - "signed-out login form", - "authenticated shell against a backend that reports the platformAdmission facet", - "navigation resolved with all six primary platform tabs", - "backend stopped mid-session, shell showing a disconnected socket", - "backend restored and the socket reconnected", - "Docker, Kubernetes, TrueNAS and vSphere platform pages populated", - "shell at 390x844 with the Proxmox table rendered" + "Default live Inbox with 12 ranked decisions, one critical item, and one governed approval waiting; Protection, verified outcomes, and history absent from the canvas", + "Protection mode with retained-objective setup only and no decision queue or activity surface", + "Activity mode with verified outcomes and the subordinate actions, findings, and run-record handoffs", + "Desktop selected-decision split with persistent queue, current position, previous/next navigation, and governed lifecycle, evidence, protection, resource, and Assistant context", + "390x844 selected-decision master/detail state with the queue hidden, Back to list, queue position, and touch-sized navigation", + "Current-coverage calm state, transport failure state, and attached availability failure without synthetic health" ], "interactions": [ - "signed in and confirmed admission is read once from a one-resource request", - "idled 20 seconds with live runtime state and confirmed no further admission requests, so the steady state polls nothing", - "stopped the backend mid-session, watched the shell report a disconnected socket, restarted it, and confirmed admission was refetched once on reconnect", - "confirmed navigation still listed every platform tab after the reconnect cycle", - "switched to the Docker tab and confirmed 80 rows", - "switched to the Kubernetes tab and confirmed 180 rows", - "switched to the TrueNAS tab and confirmed 3 rows", - "switched to the vSphere tab and confirmed 32 rows", - "returned to the Proxmox tab and confirmed 79 rows", - "resized to 390x844 and confirmed the Proxmox table still renders 79 rows", - "confirmed zero console or page errors across the pass" + "Used the live in-app Browser to inspect the final Inbox, Protection, Activity, and selected-decision split against current backend data", + "Exercised the workspace tabs and confirmed only the selected panel is present while stable Patrol route and API truth are retained", + "Verified automatic advancement after Mark reviewed, bounded suppression feedback, inbox focus restoration, deep-link reload, and no horizontal overflow", + "Passed the complete 18-scenario attention-workbench matrix across Chromium, mobile Chrome, and mobile Safari; the two corrected exact-name checks were rerun across all three engines", + "Passed all eight affected Assistant, monitor-first, and attached-availability journeys in Chromium" ] -} \ No newline at end of file +} diff --git a/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx b/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx index a0e2d7b16..12a90fd36 100644 --- a/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx +++ b/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx @@ -127,13 +127,14 @@ export function PatrolAttentionWorkbench( props.autonomyLocked ? 'monitor' : (props.autonomyLevel ?? 'monitor'), ); const autonomyExperience = createMemo(() => PATROL_AUTONOMY_EXPERIENCE[effectiveAutonomyLevel()]); - const attention = createMemo(() => - partitionPatrolAttention( - patrolAttentionStore.items(), + const attention = createMemo(() => { + const items = patrolAttentionStore.items(); + return partitionPatrolAttention( + Array.isArray(items) ? items : [], props.autonomyLevel ?? 'monitor', props.autonomyLocked ?? false, - ), - ); + ); + }); const sortedDecisions = createMemo(() => sortPatrolAttentionDecisions(attention().needsUser)); const orderedDecisions = createMemo(() => { const current = sortedDecisions(); @@ -165,9 +166,6 @@ export function PatrolAttentionWorkbench( const index = selectedDecisionIndex(); return index >= 0 ? orderedDecisions()[index + 1] : undefined; }); - const criticalDecisionCount = createMemo( - () => attention().needsUser.filter((decision) => decision.item.severity === 'critical').length, - ); const briefingHeadline = createMemo(() => { if (patrolAttentionStore.loading() && !summary()) return 'Building your current briefing'; const count = attention().needsUser.length; @@ -310,105 +308,58 @@ export function PatrolAttentionWorkbench( } }); - const decisionCountLabel = createMemo(() => { - const count = attention().needsUser.length; - return `${count} ${count === 1 ? 'item requires' : 'items require'} review`; - }); - return (
-
-
+
+
-

- Today's Patrol briefing -

-

- Needs your attention -

-

+

{briefingHeadline()} -

-

+

+

{attention().needsUser.length > 0 - ? 'Patrol has already ordered the queue by severity, actionability, and the freshest evidence.' + ? 'Highest priority first, based on severity, actionability, and current evidence.' : autonomyExperience().needsYouDescription}

-
-
- - {(decision) => ( - - )} - - 0}> - - - - -
- -
-
- - {criticalDecisionCount()} - - - Critical - -
-
- - {attention().needsUser.length} - - - Decisions - -
-
- - {props.pendingActionCount ?? 0} - - - Approvals - -
-
+
+ + {(decision) => ( + + )} + 0}> + + + +
0}> @@ -433,22 +384,11 @@ export function PatrolAttentionWorkbench(
0 ? 'lg:grid-cols-[minmax(20rem,0.78fr)_minmax(0,1.22fr)]' : ''}`} + class={`grid min-w-0 ${selectedItemId() ? 'lg:grid-cols-[minmax(20rem,0.78fr)_minmax(0,1.22fr)]' : ''}`} >
0 ? 'lg:max-h-[52rem] lg:overflow-y-auto lg:border-r lg:border-border' : ''}`} + class={`min-w-0 ${selectedItemId() ? 'hidden lg:block lg:max-h-[52rem] lg:overflow-y-auto lg:border-r lg:border-border' : ''}`} > - 0}> -
-
-

Decision inbox

-

Highest priority first

-
- - {attention().needsUser.length} open - -
-
- - {(decision) => ( - - )} - - } - > +
; onSelect: (itemId: string) => void; }) { + const hasQuietWork = () => { + const items = patrolAttentionStore.items(); + return Array.isArray(items) && items.length > 0; + }; + return (
@@ -604,7 +514,7 @@ function AttentionList(props: { > 0} - fallback={ 0} />} + fallback={} >
    @@ -621,7 +531,7 @@ function AttentionList(props: { if (item.verificationState === 'failed' || item.verificationState === 'unknown') { return 'Verify result'; } - return 'Review'; + return undefined; }; return (
  • @@ -646,18 +556,21 @@ function AttentionList(props: { {item.title} -

{item.plainLanguageSummary}

- - {decisionLabel()} - + + {(label) => ( + + {label()} + + )} + {item.subjectResourceName} diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx index 3a97a8362..ce912b836 100644 --- a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx +++ b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx @@ -301,8 +301,8 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState class="relative z-[200] mb-3" /> -
-
+
+
{renderRunControl( - 'flex min-h-11 items-center justify-center gap-2 rounded-md border border-blue-600 bg-blue-600 px-3 py-1.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-blue-700 disabled:border-border disabled:bg-surface-alt disabled:text-muted sm:min-h-0', + 'flex min-h-11 items-center justify-center gap-2 rounded-md border border-border bg-surface px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover disabled:bg-surface-alt disabled:text-muted sm:min-h-0', )} Settings @@ -376,8 +376,8 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
-
- +
+ Mode and automation
diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx index 88429e94c..efece3dd6 100644 --- a/frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx +++ b/frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx @@ -14,12 +14,38 @@ import { PatrolAttentionWorkbench } from './PatrolAttentionWorkbench'; import { PatrolObjectivesPanel } from './PatrolObjectivesPanel'; import { PatrolRecentWorkPanel } from './PatrolRecentWorkPanel'; +type PatrolWorkspaceView = 'inbox' | 'protection' | 'activity'; +const PATROL_WORKSPACE_VIEWS: readonly PatrolWorkspaceView[] = ['inbox', 'protection', 'activity']; + export function PatrolIntelligenceSurface() { const state = usePatrolIntelligenceState(); + const [activeView, setActiveView] = createSignal('inbox'); const [findingsOpen, setFindingsOpen] = createSignal(false); + const workspaceTabs: Partial> = {}; let findingsPanel: HTMLDetailsElement | undefined; const openWorkCount = () => aiIntelligenceStore.patrolOpenWorkCount; + const activateView = (view: PatrolWorkspaceView, focus = false) => { + setActiveView(view); + if (focus) queueMicrotask(() => workspaceTabs[view]?.focus()); + }; + const handleWorkspaceKeyDown = (event: KeyboardEvent, currentView: PatrolWorkspaceView) => { + const currentIndex = PATROL_WORKSPACE_VIEWS.indexOf(currentView); + const requestedIndex = + event.key === 'ArrowRight' + ? (currentIndex + 1) % PATROL_WORKSPACE_VIEWS.length + : event.key === 'ArrowLeft' + ? (currentIndex - 1 + PATROL_WORKSPACE_VIEWS.length) % PATROL_WORKSPACE_VIEWS.length + : event.key === 'Home' + ? 0 + : event.key === 'End' + ? PATROL_WORKSPACE_VIEWS.length - 1 + : -1; + if (requestedIndex < 0) return; + event.preventDefault(); + activateView(PATROL_WORKSPACE_VIEWS[requestedIndex], true); + }; const openFindings = () => { + activateView('activity'); setFindingsOpen(true); queueMicrotask(() => { findingsPanel?.scrollIntoView?.({ block: 'start' }); @@ -31,120 +57,171 @@ export function PatrolIntelligenceSurface() {
- - -
- - +
+ {PATROL_WORKSPACE_VIEWS.map((view) => ( + + ))}
-
-
-

- Review and history -

-

- Follow pending decisions, audit completed operations, or inspect Patrol's underlying - records. -

-
-
- +
+ + +
+ } > - - - - - - Actions and approvals - 0}> - - {actionInboxStore.pendingActionCount} waiting - - - - - Review governed actions and their complete audit trail. - - - -
- - +
-
+ -
setFindingsOpen(event.currentTarget.open)} - > - Findings and run records + +
+ +
+
+ +
- + + +
+
+

+ Review and history +

+

+ Audit governed operations or inspect Patrol's underlying records. +

+
+
+ + + + + + + Actions and approvals + 0}> + + {actionInboxStore.pendingActionCount} waiting + + + + + Review governed actions and their complete audit trail. + + + + + + +
+
+ +
setFindingsOpen(event.currentTarget.open)} + > + Findings and run records +
+ +
+
-
+
); } diff --git a/frontend-modern/src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx b/frontend-modern/src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx index 14af37435..b4641bab6 100644 --- a/frontend-modern/src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx +++ b/frontend-modern/src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx @@ -230,16 +230,17 @@ describe('PatrolAttentionWorkbench', () => { renderWorkbench(); expect( - await screen.findByRole('heading', { name: 'Needs your attention' }), + await screen.findByRole('heading', { name: '1 decision needs you' }), ).toBeInTheDocument(); - expect(screen.getByText('1 decision needs you')).toBeInTheDocument(); - expect(screen.getByText("Today's Patrol briefing")).toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'Decision inbox' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Start review' })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Review this decision' })).toBeInTheDocument(); expect( screen.getByRole('button', { name: 'Open Disk pressure on Database VM' }), ).toBeInTheDocument(); + expect(screen.queryByText("Today's Patrol briefing")).not.toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: 'Decision inbox' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Review this decision' })).not.toBeInTheDocument(); + expect(screen.queryByText('Open', { exact: true })).not.toBeInTheDocument(); + expect(screen.queryByText('Review', { exact: true })).not.toBeInTheDocument(); expect(screen.queryByRole('group', { name: 'Attention state' })).not.toBeInTheDocument(); expect(screen.queryByRole('combobox', { name: 'Attention state' })).not.toBeInTheDocument(); expect(apiMocks.getList).toHaveBeenCalledWith('active'); diff --git a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts index 9c3e5db91..d2404c7d5 100644 --- a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts @@ -103,10 +103,10 @@ describe('PatrolIntelligenceHeader', () => { it('keeps primary Patrol actions touch-sized on phones without inflating desktop chrome', () => { expect(headerSource).toContain( - 'flex min-h-11 items-center justify-center gap-2 rounded-md border border-blue-600', + 'flex min-h-11 items-center justify-center gap-2 rounded-md border border-border', ); expect(headerSource).toContain( - 'flex min-h-11 items-center justify-center gap-2 rounded-md border border-border', + 'flex min-h-11 items-center justify-center gap-2 rounded-md border border-transparent', ); expect(headerSource).toContain('sm:min-h-0'); }); diff --git a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx index f896c7424..d93d90c59 100644 --- a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx +++ b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx @@ -535,6 +535,13 @@ const defaultOperationsLoopStatus = (overrides: Record = {}) => ...overrides, }); +async function openPatrolActivityMode() { + await waitFor(() => { + expect(screen.getByRole('tab', { name: 'Activity' })).toBeInTheDocument(); + }); + fireEvent.click(screen.getByRole('tab', { name: 'Activity' })); +} + describe('AIIntelligence entitlement gating', () => { it('keeps Patrol page data sync bounded without making it a primary action', () => { expect(patrolIntelligenceStateSource).toContain('PATROL_REFRESH_TIMEOUT_MS'); @@ -729,7 +736,9 @@ describe('AIIntelligence entitlement gating', () => { expect(screen.queryByRole('link', { name: 'View plans' })).not.toBeInTheDocument(); expect(patrolControlAnchor).toContainElement(operationsLoopAnchor); expect(screen.queryByTestId('patrol-current-work')).not.toBeInTheDocument(); - expect(screen.getByText('Current Patrol issues appear here.')).toBeInTheDocument(); + expect(screen.queryByText('Current Patrol issues appear here.')).not.toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'Inbox' })).toHaveAttribute('aria-selected', 'true'); + expect(screen.getByRole('region', { name: 'Patrol decision inbox' })).toBeInTheDocument(); expect( screen.queryByText('Issues Patrol found. Infrastructure stays unchanged.'), ).not.toBeInTheDocument(); @@ -887,7 +896,7 @@ describe('AIIntelligence entitlement gating', () => { render(() => ); await waitFor(() => { - expect(screen.getByRole('heading', { name: 'Open work' })).toBeInTheDocument(); + expect(screen.getAllByRole('button', { name: /Check now/i }).length).toBeGreaterThan(0); }); getCorrelationsMock.mockImplementation(() => new Promise(() => {})); @@ -914,7 +923,7 @@ describe('AIIntelligence entitlement gating', () => { render(() => ); await waitFor(() => { - expect(screen.getByRole('heading', { name: 'Open work' })).toBeInTheDocument(); + expect(screen.getAllByRole('button', { name: /Check now/i }).length).toBeGreaterThan(0); }); fireEvent.click(screen.getAllByRole('button', { name: /Check now/i })[0]); @@ -1063,6 +1072,7 @@ describe('AIIntelligence entitlement gating', () => { expect(screen.queryByText('VM 200')).not.toBeInTheDocument(); expect(screen.queryByText('Disk Full → Restart')).not.toBeInTheDocument(); + await openPatrolActivityMode(); fireEvent.click(screen.getByRole('button', { name: 'History' })); await waitFor(() => { expect(screen.getByRole('heading', { name: 'Patrol history' })).toBeInTheDocument(); @@ -1494,7 +1504,7 @@ describe('AIIntelligence entitlement gating', () => { await waitFor(() => { expect(screen.getByRole('heading', { name: 'Patrol' })).toBeInTheDocument(); - expect(screen.getByText('Open work')).toBeInTheDocument(); + expect(screen.getByRole('region', { name: 'Patrol decision inbox' })).toBeInTheDocument(); }); expect(screen.queryByText('Loading view...')).not.toBeInTheDocument(); @@ -1514,7 +1524,7 @@ describe('AIIntelligence entitlement gating', () => { await waitFor(() => { expect(screen.getByRole('heading', { name: 'Patrol' })).toBeInTheDocument(); expect(screen.getByText('Patrol could not refresh')).toBeInTheDocument(); - expect(screen.getByText('Open work')).toBeInTheDocument(); + expect(screen.getByRole('region', { name: 'Patrol decision inbox' })).toBeInTheDocument(); }); expect(screen.queryByText('Loading view...')).not.toBeInTheDocument(); @@ -1785,6 +1795,7 @@ describe('AIIntelligence entitlement gating', () => { }; render(() => ); + await openPatrolActivityMode(); await waitFor(() => { expect(screen.getByText('Open work')).toBeInTheDocument(); @@ -2012,7 +2023,7 @@ describe('AIIntelligence entitlement gating', () => { expect(screen.queryByTestId('patrol-status-bar')).not.toBeInTheDocument(); expect(screen.queryByRole('heading', { name: 'Open work' })).not.toBeInTheDocument(); - expect(screen.getByRole('heading', { name: 'Needs your attention' })).toBeInTheDocument(); + expect(screen.queryByRole('region', { name: 'Patrol decision inbox' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Active' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'All' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Resolved' })).not.toBeInTheDocument(); @@ -2061,7 +2072,7 @@ describe('AIIntelligence entitlement gating', () => { await waitFor(() => { expect(screen.getByText('Patrol model issue')).toBeInTheDocument(); }); - expect(screen.getByRole('heading', { name: 'Open work' })).toBeInTheDocument(); + expect(screen.getByRole('region', { name: 'Patrol decision inbox' })).toBeInTheDocument(); const providerActions = screen.getAllByRole('link', { name: /Check Patrol model/i }); expect(providerActions.length).toBeGreaterThan(0); expect(providerActions[0]).toHaveAttribute('href', '/settings/pulse-intelligence/patrol'); @@ -2378,6 +2389,7 @@ describe('AIIntelligence entitlement gating', () => { }; render(() => ); + await openPatrolActivityMode(); await waitFor(() => { expect(getPatrolStatusMock).toHaveBeenCalled(); @@ -2444,6 +2456,7 @@ describe('AIIntelligence entitlement gating', () => { }; render(() => ); + await openPatrolActivityMode(); await waitFor(() => { expect(getPatrolStatusMock).toHaveBeenCalled(); @@ -2513,6 +2526,7 @@ describe('AIIntelligence entitlement gating', () => { }; render(() => ); + await openPatrolActivityMode(); await waitFor(() => { expect(getPatrolStatusMock).toHaveBeenCalled(); @@ -2574,6 +2588,7 @@ describe('AIIntelligence entitlement gating', () => { }; render(() => ); + await openPatrolActivityMode(); await waitFor(() => { expect(getPatrolStatusMock).toHaveBeenCalled(); diff --git a/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts b/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts index 47e421a6e..2e7a2684e 100644 --- a/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts +++ b/tests/integration/tests/73-patrol-assistant-operator-briefing.spec.ts @@ -604,7 +604,10 @@ test.describe("Patrol Assistant operator briefing", () => { }); await page.goto("/patrol", { waitUntil: "domcontentloaded" }); - await page.getByRole("button", { name: /Findings and run records/ }).click(); + await page.getByRole("tab", { name: "Activity", exact: true }).click(); + await page + .getByRole("button", { name: /Findings and run records/ }) + .click(); await page.getByText("High CPU usage").click(); const findingReview = page.locator( @@ -740,7 +743,10 @@ test.describe("Patrol Assistant operator briefing", () => { includePendingApproval = false; await page.reload({ waitUntil: "domcontentloaded" }); - await page.getByRole("button", { name: /Findings and run records/ }).click(); + await page.getByRole("tab", { name: "Activity", exact: true }).click(); + await page + .getByRole("button", { name: /Findings and run records/ }) + .click(); const queuedFindingTitle = page.getByText("High CPU usage").first(); await expect(queuedFindingTitle).toBeVisible(); await queuedFindingTitle.click(); @@ -770,14 +776,19 @@ test.describe("Patrol Assistant operator briefing", () => { includeUnifiedInvestigationRecord = false; includeInvestigationProposedFix = true; await page.reload({ waitUntil: "domcontentloaded" }); - await page.getByRole("button", { name: /Findings and run records/ }).click(); + await page.getByRole("tab", { name: "Activity", exact: true }).click(); + await page + .getByRole("button", { name: /Findings and run records/ }) + .click(); const expiredFindingTitle = page.getByText("High CPU usage").first(); await expect(expiredFindingTitle).toBeVisible(); await expiredFindingTitle.click(); const expiredFinding = page.locator( "#finding-finding-operator-briefing-details", ); - await expect(expiredFinding.getByText("Action details unavailable")).toBeVisible(); + await expect( + expiredFinding.getByText("Action details unavailable"), + ).toBeVisible(); await expiredFinding.getByText("Manage", { exact: true }).click(); await expiredFinding .getByRole("button", { name: "Open in Assistant" }) 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 d5731fbfc..b49959381 100644 --- a/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts +++ b/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts @@ -293,8 +293,10 @@ const buildAttentionItem = ( eligibility: "eligible", reasons: [], evidenceIds: ["evidence-high-cpu"], - expectedPostcondition: "The agent reconnects and reports current CPU evidence.", - verificationPolicy: "Confirm a fresh agent heartbeat after restart.", + expectedPostcondition: + "The agent reconnects and reports current CPU evidence.", + verificationPolicy: + "Confirm a fresh agent heartbeat after restart.", requiresApproval: true, }, ] @@ -714,7 +716,7 @@ test.describe("Monitor-first Patrol workbench browser contract", () => { page.getByRole("heading", { level: 1, name: "Patrol" }), ).toBeVisible(); await expect( - page.getByRole("heading", { level: 2, name: "Needs your attention" }), + page.getByRole("heading", { level: 2, name: "No decisions are waiting" }), ).toBeVisible(); await expect(page.getByText("Nothing needs you right now")).toBeVisible(); await expect( @@ -760,7 +762,7 @@ test.describe("Monitor-first Patrol workbench browser contract", () => { await page.getByRole("tab", { name: /Patrol/ }).click(); await expect(page).toHaveURL(/\/patrol$/); await expect( - page.getByRole("heading", { level: 2, name: "Needs your attention" }), + page.getByRole("heading", { level: 2, name: "1 decision needs you" }), ).toBeVisible(); await expect( page.getByRole("list", { name: "Patrol attention items" }), @@ -882,7 +884,10 @@ test.describe("Monitor-first Patrol workbench browser contract", () => { await expect( page.getByRole("heading", { level: 1, name: "Patrol" }), ).toBeVisible(); - await page.getByRole("button", { name: /Findings and run records/ }).click(); + await page.getByRole("tab", { name: "Activity", exact: true }).click(); + await page + .getByRole("button", { name: /Findings and run records/ }) + .click(); await expect( page.getByText("Operating system updates need review").first(), ).toBeVisible(); @@ -926,7 +931,10 @@ test.describe("Monitor-first Patrol workbench browser contract", () => { }), ); await page.reload({ waitUntil: "domcontentloaded" }); - await page.getByRole("button", { name: /Findings and run records/ }).click(); + await page.getByRole("tab", { name: "Activity", exact: true }).click(); + await page + .getByRole("button", { name: /Findings and run records/ }) + .click(); const cleanupTitle = page .getByText("Downloaded package data is using needed space") .first(); @@ -981,7 +989,10 @@ test.describe("Monitor-first Patrol workbench browser contract", () => { }), ); await page.reload({ waitUntil: "domcontentloaded" }); - await page.getByRole("button", { name: /Findings and run records/ }).click(); + await page.getByRole("tab", { name: "Activity", exact: true }).click(); + await page + .getByRole("button", { name: /Findings and run records/ }) + .click(); await page.getByRole("button", { name: "Resolved", exact: true }).click(); const resolvedTitle = page .getByText("Operating system updates confirmed complete") diff --git a/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts b/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts index 31a110af0..3b2f7ecb4 100644 --- a/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts +++ b/tests/integration/tests/91-operational-trust-attention-workbench.spec.ts @@ -686,32 +686,60 @@ async function mockAttention( test("starts from the normal monitor shell and reaches the canonical attention queue", async ({ page, -}, testInfo) => { +}) => { await mockAttention(page, "active"); await page.goto("/", { waitUntil: "domcontentloaded" }); - const patrolNavigation = testInfo.project.name.startsWith("mobile-") - ? page - .getByRole("navigation", { name: "Mobile navigation" }) - .getByRole("button", { name: /Patrol/ }) - : page - .getByRole("tab", { name: /Patrol/ }) - .or(page.getByRole("link", { name: /Patrol/ })); + const patrolNavigation = + (page.viewportSize()?.width ?? 1280) < 1024 + ? page + .getByRole("navigation", { name: "Mobile navigation" }) + .getByRole("button", { name: /Patrol/ }) + : page + .getByRole("tab", { name: /Patrol/ }) + .or(page.getByRole("link", { name: /Patrol/ })); await patrolNavigation.click(); await expect(page).toHaveURL(/\/patrol/); await expect( - page.getByRole("region", { name: "Needs your attention" }), + page.getByRole("region", { name: "Patrol decision inbox" }), ).toBeVisible(); await expect( page.getByRole("button", { name: "Open CPU pressure on pve-main" }), ).toBeVisible(); - await expect(page.getByText("2 decisions need you")).toBeVisible(); - if (!testInfo.project.name.startsWith("mobile-")) { - await expect( - page.getByRole("button", { name: "Review this decision" }), - ).toBeVisible(); - } + await expect( + page.getByRole("tab", { name: "Inbox", exact: true }), + ).toHaveAttribute("aria-selected", "true"); + await expect( + page.getByRole("heading", { name: "Protected outcomes", exact: true }), + ).toHaveCount(0); + await expect( + page.getByRole("heading", { name: "Verified outcomes", exact: true }), + ).toHaveCount(0); + + await page.getByRole("tab", { name: "Protection", exact: true }).click(); + await expect( + page.getByRole("heading", { name: "Protected outcomes", exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("region", { name: "Patrol decision inbox" }), + ).toHaveCount(0); + + await page.getByRole("tab", { name: "Activity", exact: true }).click(); + await expect( + page.getByRole("heading", { name: "Verified outcomes", exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Review and history", exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Protected outcomes", exact: true }), + ).toHaveCount(0); + + await page.getByRole("tab", { name: "Inbox", exact: true }).click(); + await expect( + page.getByRole("region", { name: "Patrol decision inbox" }), + ).toBeVisible(); }); test("makes active operational work primary and preserves the evidence boundary", async ({ @@ -727,8 +755,7 @@ test("makes active operational work primary and preserves the evidence boundary" }), ), ).toBeVisible(); - const queue = page.getByRole("region", { name: "Needs your attention" }); - await expect(queue.getByLabel("2 items require review")).toBeVisible(); + const queue = page.getByRole("region", { name: "Patrol decision inbox" }); const attentionList = queue.getByRole("list", { name: "Patrol attention items", }); diff --git a/tests/integration/tests/92-operational-trust-availability-facet.spec.ts b/tests/integration/tests/92-operational-trust-availability-facet.spec.ts index 8c5734b19..f6240d23a 100644 --- a/tests/integration/tests/92-operational-trust-availability-facet.spec.ts +++ b/tests/integration/tests/92-operational-trust-availability-facet.spec.ts @@ -587,7 +587,7 @@ test.describe("Operational trust availability resource facet", () => { await page.goto("/patrol", { waitUntil: "domcontentloaded" }); - const queue = page.getByRole("region", { name: "Needs your attention" }); + const queue = page.getByRole("region", { name: "Patrol decision inbox" }); await expect(queue).toBeVisible({ timeout: 30_000 }); await expect( page.getByRole("tab", {