Make Patrol finding outcomes discoverable

This commit is contained in:
pulse-triage[bot]
2026-08-30 11:25:06 +01:00
parent 1e805afc4b
commit 21367b655d
7 changed files with 130 additions and 60 deletions
+16 -26
View File
@@ -1,28 +1,19 @@
{
"version": 1,
"base_sha": "87fda2a235a34bf62071458795237856ac16650d",
"verified_at": "2026-08-30T08:55:33Z",
"base_sha": "1e805afc4b54c25ed253497e4818037476cbce23",
"verified_at": "2026-08-30T10:22:47Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/api/resources.ts",
"frontend-modern/src/components/Settings/DataHandlingPanel.tsx",
"frontend-modern/src/components/Settings/ResourcePicker.tsx",
"frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts",
"frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts",
"frontend-modern/src/hooks/useResourceStats.ts",
"frontend-modern/src/hooks/useResources.ts"
"frontend-modern/src/components/AI/FindingsPanel.tsx",
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx",
"frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx"
],
"content_sha256": {
"frontend-modern/src/api/resources.ts": "08a29f4efc054188e378f5eecb94c2cad4884c8a20fc31f97c3242a72099cdde",
"frontend-modern/src/components/Settings/DataHandlingPanel.tsx": "ce04993e9a017890a7098cf15ee518701d3ecbbbd6eac91520c0c7674a86c98c",
"frontend-modern/src/components/Settings/ResourcePicker.tsx": "2e33b23cfd257222996a2adb9102b80f36ed26d0039c449c3111162ed7da13b7",
"frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts": "f9fca9e1b66f7d2f020a52a0e873da7371f649c4f37c8a7df08e627b2a1e77f2",
"frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts": "744ffef0c69a2113d12192698258e4296fd8a03fdf9a227bc9c0ab0ed250b5d7",
"frontend-modern/src/hooks/useResourceStats.ts": "b3d4e3777096dd1933a2e2c53af822052ddddb4563a9ccd1bc4c16677d191ec6",
"frontend-modern/src/hooks/useResources.ts": "142ee968fdfdeb1281feb6b04beee23c1556db42a118e47e8063b6f151d1c24a"
"frontend-modern/src/components/AI/FindingsPanel.tsx": "bddd32ceb519ad2d4deeb464c47e96789b12e512ab127617c6d7f01d3e32bbd8",
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "34540499fb66c378bba88acae22eaf48f0444e6bd0dd5c4c00a05ae93271d48a",
"frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx": "36711b7bc41597a240109c37e50174011c3b3fb5b77aa65179e1bc6cf7165524"
},
"routes": [
"/settings/security-data-handling",
"/patrol"
],
"viewports": [
@@ -36,16 +27,15 @@
}
],
"states": [
"Resource Privacy cold direct load settled with 1,855 governed resources and rendered the aggregate posture without a false empty state",
"Resource Privacy desktop and narrow layouts retained the complete policy summary, posture cards, scrolling, and viewport fit without horizontal overflow",
"Patrol Protection objective dialog opened the resource scope picker with 1,503 populated resources instead of an empty placeholder",
"resource picker desktop and narrow layouts retained search, type filters, bounded list scrolling, selection state, dialog controls, and viewport fit"
"Patrol Inbox selected-decision lifecycle showed the lasting finding outcome explanation and visible Open finding options handoff",
"Patrol Activity explained the finding outcome workflow and rendered a visible Finding options control on each active finding",
"expanded active finding showed Resolve or dismiss with Dismiss: Not an issue, Remember as expected, Dismiss: Later, and Create rule from this",
"desktop and narrow layouts retained readable controls, scrolling, and exact viewport fit without horizontal overflow"
],
"interactions": [
"loaded Resource Privacy as a cold direct route against the exact isolated frontend and backend build",
"traced the cold Resource Privacy request set and confirmed only the compact /api/resources/stats read plus one type=agent settings-status page, with no unfiltered paginated resource hydration",
"opened Patrol Protection, opened Add objective, expanded Limit to specific resources, searched for esxi-07.lab.local, and selected the matching resource",
"dismissed the objective dialog and confirmed focus returned to Add objective",
"inspected Resource Privacy and the expanded resource picker at 1440 by 900 and 390 by 844 for populated state, responsive fit, clipping, scrolling, and control availability"
"opened the first mock-backed Patrol Inbox decision and followed Open finding options into Activity",
"opened an active finding's outcome controls by pointer at desktop width and by keyboard Enter at narrow width",
"closed Resolve or dismiss and confirmed the Finding options trigger returned to aria-expanded false",
"inspected the Inbox handoff, Activity rows, expanded finding details, management disclosure, control placement, clipping, and scrolling at 1440 by 900 and 390 by 844"
]
}
@@ -189,6 +189,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
const [filter, setFilter] = createSignal<FindingsPanelFilter>(props.filterOverride ?? 'active');
const [sortBy, setSortBy] = createSignal<'severity' | 'time'>('severity');
const [expandedId, setExpandedId] = createSignal<string | null>(null);
const [manageOpenId, setManageOpenId] = createSignal<string | null>(null);
const [actionLoading, setActionLoading] = createSignal<string | null>(null);
const [lastHashScrolled, setLastHashScrolled] = createSignal<string | null>(null);
const [editingNoteId, setEditingNoteId] = createSignal<string | null>(null);
@@ -501,6 +502,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
createEffect(() => {
if (isPatrolFindingsSource() && expandedId() && !selectedPatrolFinding()) {
setExpandedId(null);
setManageOpenId(null);
}
});
const filterOptions = createMemo(() => buildFindingFilterOptions(filterCounts()));
@@ -979,12 +981,21 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
const toggleExpanded = () => {
if (expandedId() === finding.id) {
setExpandedId(null);
setManageOpenId(null);
} else {
setExpandedId(finding.id);
setManageOpenId(null);
}
props.onFindingClick?.(finding);
};
const openFindingOptions = (event: MouseEvent) => {
event.stopPropagation();
setExpandedId(finding.id);
setManageOpenId(finding.id);
props.onFindingClick?.(finding);
};
return (
<div
id={`finding-${finding.id}`}
@@ -1240,7 +1251,7 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
</Show>
</div>
{/* Actions */}
<div class="flex items-center gap-1 shrink-0">
<div class="flex shrink-0 flex-col items-end gap-1 sm:flex-row sm:items-center">
<Show
when={
!isPatrolFindingsSource() && expandedId() !== finding.id
@@ -1326,6 +1337,26 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
</button>
</Show>
</Show>
<Show
when={
isPatrolFindingsSource() &&
props.showControls !== false &&
finding.status === 'active' &&
manualControls.dismiss
}
>
<button
type="button"
aria-label={`Open resolve and dismiss options for ${title.label}`}
aria-expanded={manageOpenId() === finding.id}
aria-controls={`finding-${finding.id}-manage`}
onClick={openFindingOptions}
class="inline-flex min-h-11 items-center rounded border border-border bg-surface px-2 py-1 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 sm:min-h-0"
title="Resolve, dismiss, remember as expected, or create a rule"
>
Finding options
</button>
</Show>
<button
type="button"
aria-label={
@@ -1693,9 +1724,14 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
</Show>
<Show when={shouldShowExpandedManageMenu}>
<details onClick={(e) => e.stopPropagation()}>
<summary class="list-none cursor-pointer rounded border border-border bg-surface px-3 py-1.5 font-medium text-base-content hover:bg-surface-hover">
Manage
<details
id={`finding-${finding.id}-manage`}
open={manageOpenId() === finding.id}
onClick={(e) => e.stopPropagation()}
onToggle={(event) => setManageOpenId(event.currentTarget.open ? finding.id : null)}
>
<summary class="list-none cursor-pointer rounded border border-border bg-surface px-3 py-1.5 font-medium text-base-content hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40">
Resolve or dismiss
</summary>
<div class="mt-1 flex min-w-48 flex-col gap-1 rounded border border-border bg-surface p-1 shadow-sm">
<Show when={shouldShowAssistantManageAction}>
@@ -2199,7 +2235,10 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
<button
type="button"
aria-label={`Close review panel for ${title().label}`}
onClick={() => setExpandedId(null)}
onClick={() => {
setExpandedId(null);
setManageOpenId(null);
}}
class="rounded p-1.5 text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/40"
>
<XIcon class="h-4 w-4" />
@@ -329,7 +329,7 @@ describe('FindingsPanel resource links', () => {
expect(
screen.getByRole('complementary', { name: 'Review Provider connection issue' }),
).toBeInTheDocument();
expect(screen.getByText('Manage')).toBeInTheDocument();
expect(screen.getByText('Resolve or dismiss')).toBeInTheDocument();
const assistantHandoff = screen.getByRole('button', { name: 'Open in Assistant' });
expect(assistantHandoff.closest('details')).not.toBeNull();
expect(screen.getByRole('button', { name: 'Acknowledge' })).toBeInTheDocument();
@@ -565,6 +565,31 @@ describe('FindingsPanel resource links', () => {
expectCollapsedFinding('Provider connection issue');
});
it('opens durable finding outcomes directly from an active Patrol row', async () => {
render(() => <FindingsPanel findingsSource="patrol" />);
await waitFor(() => expect(mockState.loadPatrolFindings).toHaveBeenCalled());
const options = screen.getByRole('button', {
name: 'Open resolve and dismiss options for Provider connection issue',
});
expect(options).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(options);
expect(options).toHaveAttribute('aria-expanded', 'true');
expect(screen.getByText('Resolve or dismiss')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Dismiss: Not an issue' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Remember as expected' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Dismiss: Later' })).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Create rule from this' })).toBeInTheDocument();
fireEvent.click(
screen.getByRole('button', { name: 'Close review panel for Provider connection issue' }),
);
expect(options).toHaveAttribute('aria-expanded', 'false');
});
it('renders Patrol findings while the unified findings request is still loading', async () => {
mockState.findingsLoading = true;
mockState.patrolFindingsLoading = false;
@@ -223,7 +223,7 @@ describe('FindingsPanel assistant handoff', () => {
expect(findingsPanelSource).toMatch(/>\s*Copy summary\s*</);
});
it('keeps expanded finding actions behind a canonical primary action and compact Manage menu', () => {
it('keeps expanded finding actions behind a canonical primary action and explicit outcome menu', () => {
expect(findingsPanelSource).toContain('getFindingPrimaryActionPresentation(finding)');
expect(findingsPanelSource).toContain('getPrimaryAssistantFindingAction');
expect(findingsPanelSource).toContain('const shouldShowAssistantPrimaryAction =');
@@ -235,7 +235,7 @@ describe('FindingsPanel assistant handoff', () => {
expect(findingsPanelSource).not.toContain(
'fallback={\n <button\n type="button"\n onClick={(e) => {\n e.stopPropagation();\n void openFindingInAssistant(finding);',
);
expect(findingsPanelSource).toContain('Manage');
expect(findingsPanelSource).toContain('Resolve or dismiss');
expect(findingsPanelSource).toContain('min-w-48');
expect(findingsPanelSource).not.toContain('min-w-40');
});
@@ -243,7 +243,7 @@ describe('FindingsPanel assistant handoff', () => {
it('keeps primary-action Patrol runtime findings out of secondary action clutter', () => {
// Runtime/setup findings already have one canonical recovery path:
// provider settings. The expanded card must not add the Assistant
// secondary button or generic Manage menu beside that primary action.
// secondary button or generic outcome menu beside that primary action.
expect(findingsPanelSource).toContain('const shouldShowExpandedManageMenu =');
expect(findingsPanelSource).toContain('!primaryAction ||');
expect(findingsPanelSource).toContain('<Show when={shouldShowExpandedManageMenu}>');
@@ -1343,6 +1343,22 @@ function AttentionLifecycleControls(props: {
</Button>
</Show>
</div>
<Show when={props.onOpenFindings}>
<div class="mt-3 flex flex-col gap-3 rounded-md border border-border-subtle bg-surface px-3 py-3 sm:flex-row sm:items-center sm:justify-between">
<p class="text-xs leading-5 text-muted">
Need a lasting Patrol finding outcome? Resolve it, dismiss it as not an issue,
remember expected behavior, or create a suppression rule.
</p>
<Button
variant="secondary"
size="sm"
class="min-h-11 shrink-0 sm:min-h-0"
onClick={() => props.onOpenFindings?.()}
>
Open finding options
</Button>
</div>
</Show>
<Show when={showSuppression()}>
<form
class="mt-3 space-y-3 border-t border-border-subtle pt-3"
@@ -1421,18 +1437,7 @@ function AttentionLifecycleControls(props: {
>
adjust alert thresholds
</A>{' '}
to change or turn off this alert for the affected resource
<Show when={props.onOpenFindings} fallback=".">
, or{' '}
<button
type="button"
class="font-medium text-blue-700 hover:underline dark:text-blue-300"
onClick={() => props.onOpenFindings?.()}
>
review Patrol findings
</button>{' '}
to mark a finding as expected so Patrol stops raising it.
</Show>
to change or turn off this alert for the affected resource.
</p>
</div>
</details>
@@ -126,7 +126,7 @@ export function PatrolIntelligenceSurface() {
Review and history
</h2>
<p class="mt-1 max-w-3xl text-sm leading-5 text-muted">
Audit governed operations or inspect Patrol's underlying records.
Audit governed operations, manage finding outcomes, or inspect Patrol history.
</p>
</div>
<div class="grid divide-y divide-border sm:grid-cols-2 sm:divide-x sm:divide-y-0">
@@ -177,9 +177,12 @@ export function PatrolIntelligenceSurface() {
<HistoryIcon class="h-5 w-5" aria-hidden="true" />
</span>
<span class="min-w-0">
<span class="text-sm font-semibold text-base-content">Patrol records</span>
<span class="text-sm font-semibold text-base-content">
Finding options and history
</span>
<span class="mt-1 block text-xs font-normal leading-5 text-muted">
Inspect findings and check history when you need the forensic trail.
Resolve or dismiss findings, remember expected behavior, create rules, and
inspect check history.
</span>
</span>
</span>
@@ -199,13 +202,14 @@ export function PatrolIntelligenceSurface() {
open={findingsOpen()}
onToggle={(event) => setFindingsOpen(event.currentTarget.open)}
>
<summary class="sr-only">Patrol records</summary>
<summary class="sr-only">Finding options and history</summary>
<div
class={`space-y-4 border-t border-border p-4 sm:p-5 ${!state.patrolEnabledLocal() ? 'opacity-50 pointer-events-none' : ''}`}
>
<p class="text-xs leading-5 text-muted">
Patrol records keep raw check findings and run history. The Inbox separately lists
items that currently need a decision.
Choose Finding options on an active finding to resolve it, dismiss it, remember it
as expected, or create a suppression rule. Check history remains available here for
the forensic trail.
</p>
<PatrolIntelligenceWorkspace state={state} />
</div>
@@ -686,7 +686,7 @@ describe('PatrolAttentionWorkbench', () => {
).toBeInTheDocument();
});
it('points lifecycle users at the durable threshold and finding controls', async () => {
it('keeps durable Patrol finding outcomes visible beside alert lifecycle controls', async () => {
const active = item();
apiMocks.getList.mockResolvedValue(
listResponse([active], summary({ activeCount: 1, openCount: 1, calm: false })),
@@ -708,16 +708,23 @@ describe('PatrolAttentionWorkbench', () => {
}),
);
expect(
await screen.findByText(/Mark reviewed removes this occurrence from the decision inbox/i),
).toHaveTextContent(/until it resolves or you return it to open/i);
expect(screen.queryByText(/today's decision inbox/i)).not.toBeInTheDocument();
expect(await screen.findByText(/Need a lasting Patrol finding outcome/i)).toHaveTextContent(
/remember expected behavior/i,
);
fireEvent.click(screen.getByRole('button', { name: 'Open finding options' }));
expect(onOpenFindings).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByText('More ways to manage this issue'));
expect(screen.getByText(/Mark reviewed removes this occurrence/i)).toHaveTextContent(
/until it resolves or you return it to open/i,
);
expect(screen.getByRole('link', { name: 'adjust alert thresholds' })).toHaveAttribute(
'href',
'/alerts/thresholds',
);
fireEvent.click(screen.getByRole('button', { name: 'review Patrol findings' }));
expect(onOpenFindings).toHaveBeenCalledTimes(1);
expect(
screen.queryByRole('button', { name: 'review Patrol findings' }),
).not.toBeInTheDocument();
});
it('omits the findings pointer when no findings surface is wired', async () => {