diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index eb794b2ed..e1203279d 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -135,6 +135,7 @@ Own canonical runtime payload shapes between backend and frontend.
and the Patrol findings empty-state behavior, so `0 active findings` only renders as a healthy frontend conclusion when the same governed AI summary contract still reports healthy overall health; degraded or not-fully-verified health predictions must flow through to the Patrol findings surface instead of being replaced by page-local "looks healthy" copy
and the Patrol assessment headline plus compact summary-strip behavior, so the same governed AI summary contract decides whether the page leads with verified health, issues detected, coverage incomplete, or another attention state instead of letting count-only page fragments emit a stale `No issues found` conclusion
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 canonical alert-triggered Patrol enqueue path in `internal/api/router.go`, so alert-fired Patrol work flows through the unified alert bridge and trigger manager instead of being duplicated by monitor callback wiring
and the shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` card, so canonical recent-change timelines stay rendered through one governed frontend card instead of separate page-local list loops
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 240a55b78..ecf18512c 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -1076,6 +1076,10 @@ When the same governed run-history contract shows a recent full patrol plus
same-day scoped follow-up work, that summary shell should also carry a compact
activity-mix explanation rather than forcing operators to infer why Patrol
looked busy from a separate activity strip alone.
+That explanation belongs on the verification surface itself when operators are
+reconciling `Recently verified` copy against same-day scoped Patrol bursts; the
+status strip may support the readout, but it is not sufficient as the only
+explanation path.
That same shell rule also owns Patrol recency labels. Shared Patrol header and
status-shell surfaces must keep `Last full patrol` tied only to the full-sweep
transport fact and use `Last activity` for scoped or verification work instead
diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md
index 3e9979230..a80694727 100644
--- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md
+++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md
@@ -227,6 +227,10 @@ operator whether Patrol recently completed a successful full patrol, only ran
scoped alert-triggered checks, or ended its most recent full patrol with
errors, so the page does not leave trust and coverage as implicit background
knowledge.
+When same-day run history shows both a recent full patrol and a burst of
+scoped follow-up activity, that same verification surface should expose the
+recent activity mix explicitly instead of leaving operators to reconcile a
+`Recently verified` headline with a busy Patrol strip elsewhere on the page.
Fix-verification checks belong to that same explanation layer as targeted
activity, not as evidence of a fresh full-estate sweep.
The same hierarchy applies to investigation context. Correlations, recent
diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx
index 5a9204b69..e21074911 100644
--- a/frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx
+++ b/frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx
@@ -205,6 +205,11 @@ export function PatrolIntelligenceSummary(props: { state: PatrolIntelligenceStat
{verification().description}
+
+
+ Recent activity mix: {verification().activityMixLabel}
+
+
diff --git a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx
index 92acb7b93..dbabc4618 100644
--- a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx
+++ b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx
@@ -1029,6 +1029,142 @@ describe('AIIntelligence entitlement gating', () => {
).not.toBeInTheDocument();
});
+ it('surfaces the recent activity mix in the verification summary when scoped runs are creating noise', async () => {
+ hasFeatureMock.mockReturnValue(true);
+ licenseStatusMock.mockReturnValue({ subscription_state: 'active' });
+ getPatrolStatusMock.mockResolvedValue(defaultPatrolStatus({ license_required: false }));
+ getPatrolRunHistoryMock.mockResolvedValue([
+ {
+ id: 'run-scoped-alert',
+ started_at: '2026-03-12T10:00:00Z',
+ completed_at: '2026-03-12T10:01:00Z',
+ duration_ms: 60000,
+ type: 'scoped',
+ trigger_reason: 'alert_fired',
+ scope_resource_ids: [],
+ effective_scope_resource_ids: [],
+ scope_resource_types: [],
+ resources_checked: 1,
+ nodes_checked: 0,
+ guests_checked: 0,
+ docker_checked: 0,
+ storage_checked: 0,
+ hosts_checked: 0,
+ pbs_checked: 0,
+ pmg_checked: 0,
+ kubernetes_checked: 0,
+ new_findings: 0,
+ existing_findings: 0,
+ rejected_findings: 0,
+ resolved_findings: 0,
+ auto_fix_count: 0,
+ findings_summary: '',
+ finding_ids: [],
+ error_count: 0,
+ status: 'healthy',
+ triage_flags: 0,
+ tool_call_count: 0,
+ },
+ {
+ id: 'run-scoped-anomaly',
+ started_at: '2026-03-12T09:58:00Z',
+ completed_at: '2026-03-12T09:59:00Z',
+ duration_ms: 60000,
+ type: 'scoped',
+ trigger_reason: 'anomaly',
+ scope_resource_ids: [],
+ effective_scope_resource_ids: [],
+ scope_resource_types: [],
+ resources_checked: 1,
+ nodes_checked: 0,
+ guests_checked: 0,
+ docker_checked: 0,
+ storage_checked: 0,
+ hosts_checked: 0,
+ pbs_checked: 0,
+ pmg_checked: 0,
+ kubernetes_checked: 0,
+ new_findings: 0,
+ existing_findings: 0,
+ rejected_findings: 0,
+ resolved_findings: 0,
+ auto_fix_count: 0,
+ findings_summary: '',
+ finding_ids: [],
+ error_count: 0,
+ status: 'healthy',
+ triage_flags: 0,
+ tool_call_count: 0,
+ },
+ {
+ id: 'run-full',
+ started_at: '2026-03-12T09:50:00Z',
+ completed_at: '2026-03-12T09:57:00Z',
+ duration_ms: 420000,
+ type: 'patrol',
+ trigger_reason: 'scheduled',
+ scope_resource_ids: [],
+ effective_scope_resource_ids: [],
+ scope_resource_types: [],
+ resources_checked: 58,
+ nodes_checked: 0,
+ guests_checked: 0,
+ docker_checked: 0,
+ storage_checked: 0,
+ hosts_checked: 0,
+ pbs_checked: 0,
+ pmg_checked: 0,
+ kubernetes_checked: 0,
+ new_findings: 0,
+ existing_findings: 0,
+ rejected_findings: 0,
+ resolved_findings: 0,
+ auto_fix_count: 0,
+ findings_summary: 'No active findings',
+ finding_ids: [],
+ error_count: 0,
+ status: 'healthy',
+ triage_flags: 0,
+ tool_call_count: 0,
+ },
+ ]);
+ intelligenceState.summary = {
+ timestamp: '2026-03-12T10:05:00Z',
+ overall_health: {
+ score: 100,
+ grade: 'A',
+ trend: 'stable',
+ factors: [],
+ prediction: 'Infrastructure is healthy with no significant issues detected.',
+ },
+ findings_count: {
+ critical: 0,
+ warning: 0,
+ watch: 0,
+ info: 0,
+ total: 0,
+ },
+ predictions_count: 0,
+ recent_changes_count: 0,
+ learning: {
+ resources_with_knowledge: 0,
+ total_notes: 0,
+ resources_with_baselines: 0,
+ patterns_detected: 0,
+ correlations_learned: 0,
+ incidents_tracked: 0,
+ },
+ };
+
+ render(() => );
+
+ await waitFor(() => {
+ expect(
+ screen.getByText('Recent activity mix: 1 full, 1 alert-triggered, 1 anomaly-triggered'),
+ ).toBeInTheDocument();
+ });
+ });
+
it('treats a selected zero-finding run as an empty snapshot and uses effective scope ids', async () => {
hasFeatureMock.mockReturnValue(true);
licenseStatusMock.mockReturnValue({ subscription_state: 'active' });
diff --git a/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts b/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts
index 17f423445..229248038 100644
--- a/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts
+++ b/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts
@@ -344,6 +344,105 @@ describe('getPatrolSummaryPresentation', () => {
});
});
+ it('adds an activity mix when scoped runs make recent verification look busy', () => {
+ expect(
+ getPatrolVerificationPresentation({
+ runs: [
+ {
+ id: 'run-scoped-alert',
+ started_at: '2026-03-12T10:00:00Z',
+ completed_at: '2026-03-12T10:01:00Z',
+ duration_ms: 60000,
+ type: 'scoped',
+ trigger_reason: 'alert_fired',
+ resources_checked: 1,
+ nodes_checked: 0,
+ guests_checked: 0,
+ docker_checked: 0,
+ storage_checked: 0,
+ hosts_checked: 0,
+ pbs_checked: 0,
+ pmg_checked: 0,
+ kubernetes_checked: 0,
+ new_findings: 0,
+ existing_findings: 0,
+ rejected_findings: 0,
+ resolved_findings: 0,
+ auto_fix_count: 0,
+ findings_summary: '',
+ finding_ids: [],
+ error_count: 0,
+ status: 'healthy',
+ triage_flags: 0,
+ tool_call_count: 0,
+ },
+ {
+ id: 'run-scoped-anomaly',
+ started_at: '2026-03-12T09:58:00Z',
+ completed_at: '2026-03-12T09:59:00Z',
+ duration_ms: 60000,
+ type: 'scoped',
+ trigger_reason: 'anomaly',
+ resources_checked: 1,
+ nodes_checked: 0,
+ guests_checked: 0,
+ docker_checked: 0,
+ storage_checked: 0,
+ hosts_checked: 0,
+ pbs_checked: 0,
+ pmg_checked: 0,
+ kubernetes_checked: 0,
+ new_findings: 0,
+ existing_findings: 0,
+ rejected_findings: 0,
+ resolved_findings: 0,
+ auto_fix_count: 0,
+ findings_summary: '',
+ finding_ids: [],
+ error_count: 0,
+ status: 'healthy',
+ triage_flags: 0,
+ tool_call_count: 0,
+ },
+ {
+ id: 'run-full',
+ started_at: '2026-03-12T09:50:00Z',
+ completed_at: '2026-03-12T09:57:00Z',
+ duration_ms: 420000,
+ type: 'patrol',
+ resources_checked: 58,
+ nodes_checked: 0,
+ guests_checked: 0,
+ docker_checked: 0,
+ storage_checked: 0,
+ hosts_checked: 0,
+ pbs_checked: 0,
+ pmg_checked: 0,
+ kubernetes_checked: 0,
+ new_findings: 0,
+ existing_findings: 1,
+ rejected_findings: 0,
+ resolved_findings: 0,
+ auto_fix_count: 0,
+ findings_summary: '1 warning',
+ finding_ids: ['finding-1'],
+ error_count: 0,
+ status: 'issues_found',
+ triage_flags: 0,
+ tool_call_count: 0,
+ },
+ ] as never,
+ }),
+ ).toEqual({
+ title: 'Recently verified',
+ description: 'The most recent full patrol completed successfully and checked 58 resources.',
+ compactLabel: 'Recently verified',
+ tone: 'success',
+ lastFullRunAt: '2026-03-12T09:57:00Z',
+ activityMixLabel: '1 full, 1 alert-triggered, 1 anomaly-triggered',
+ });
+ });
+
it('reports partial verification when only scoped runs are recent', () => {
expect(
getPatrolVerificationPresentation({
diff --git a/frontend-modern/src/utils/patrolSummaryPresentation.ts b/frontend-modern/src/utils/patrolSummaryPresentation.ts
index 0f5945814..36d683886 100644
--- a/frontend-modern/src/utils/patrolSummaryPresentation.ts
+++ b/frontend-modern/src/utils/patrolSummaryPresentation.ts
@@ -2,6 +2,10 @@ import type { PatrolRunRecord, PatrolRuntimeState } from '@/api/patrol';
import type { UnifiedFinding } from '@/stores/aiIntelligence';
import type { IntelligenceHealthScore } from '@/types/aiIntelligence';
import { isPatrolRuntimeFinding } from '@/utils/aiFindingPresentation';
+import {
+ formatPatrolActivityBreakdown,
+ getPatrolActivityBreakdown,
+} from '@/utils/patrolRunPresentation';
import type { SemanticTone } from '@/utils/semanticTonePresentation';
import { getPatrolRuntimePresentation } from '@/utils/patrolRuntimePresentation';
@@ -50,6 +54,7 @@ export interface PatrolVerificationPresentation {
compactLabel: string;
tone: SemanticTone;
lastFullRunAt?: string;
+ activityMixLabel?: string;
}
export interface PatrolRecencyPresentation {
@@ -312,6 +317,28 @@ function isVerificationPatrolRun(run: PatrolRunRecord): boolean {
return normalizeRunType(run.type) === 'verification';
}
+function getVerificationActivityMixLabel(runs: PatrolRunRecord[]): string | undefined {
+ const latestCompletedRun = runs.find((run) => isCompletedPatrolRun(run));
+ const referenceTimestamp = latestCompletedRun?.completed_at || latestCompletedRun?.started_at;
+ if (!referenceTimestamp) {
+ return undefined;
+ }
+
+ const breakdown = getPatrolActivityBreakdown(runs, new Date(referenceTimestamp));
+ const scopedRuns =
+ breakdown.alertTriggeredRuns +
+ breakdown.anomalyTriggeredRuns +
+ breakdown.alertClearedRuns +
+ breakdown.verificationChecks +
+ breakdown.otherScopedRuns;
+ if (breakdown.totalRuns <= 1 || scopedRuns <= 0) {
+ return undefined;
+ }
+
+ const label = formatPatrolActivityBreakdown(breakdown);
+ return label || undefined;
+}
+
function isCompletedPatrolRun(run: PatrolRunRecord): boolean {
return Boolean(run.completed_at?.trim());
}
@@ -442,6 +469,7 @@ export function getPatrolVerificationPresentation(args: {
}
const completedRuns = (args.runs ?? []).filter((run) => isCompletedPatrolRun(run));
+ const activityMixLabel = getVerificationActivityMixLabel(completedRuns);
const recentFullRun = completedRuns.find((run) => isFullPatrolRun(run));
if (recentFullRun) {
@@ -456,6 +484,7 @@ export function getPatrolVerificationPresentation(args: {
compactLabel: 'Verification limited',
tone: 'warning',
lastFullRunAt: recentFullRun.completed_at,
+ activityMixLabel,
};
}
@@ -468,6 +497,7 @@ export function getPatrolVerificationPresentation(args: {
compactLabel: 'Recently verified',
tone: 'success',
lastFullRunAt: recentFullRun.completed_at,
+ activityMixLabel,
};
}
@@ -496,6 +526,7 @@ export function getPatrolVerificationPresentation(args: {
description,
compactLabel: 'Partial verification',
tone: 'warning',
+ activityMixLabel,
};
}
diff --git a/tests/integration/tests/18-patrol-runtime-state.spec.ts b/tests/integration/tests/18-patrol-runtime-state.spec.ts
index 14f3980bf..6a867aa0f 100644
--- a/tests/integration/tests/18-patrol-runtime-state.spec.ts
+++ b/tests/integration/tests/18-patrol-runtime-state.spec.ts
@@ -516,6 +516,11 @@ test.describe("Patrol runtime-state browser contract", () => {
"Breakdown: 1 full, 1 alert-triggered, 1 anomaly-triggered",
),
).toBeVisible();
+ await expect(
+ page.getByText(
+ "Recent activity mix: 1 full, 1 alert-triggered, 1 anomaly-triggered",
+ ),
+ ).toBeVisible();
await expect(
page.getByText("Scoped triggers: 4 queued · busy mode · anomalies off"),
).toBeVisible();