mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
Expose Patrol verification activity mix
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -205,6 +205,11 @@ export function PatrolIntelligenceSummary(props: { state: PatrolIntelligenceStat
|
||||
</Show>
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-muted">{verification().description}</p>
|
||||
<Show when={verification().activityMixLabel}>
|
||||
<p class="mt-2 text-xs font-medium text-base-content">
|
||||
Recent activity mix: {verification().activityMixLabel}
|
||||
</p>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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(() => <AIIntelligence />);
|
||||
|
||||
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' });
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user