From 9a507885a37f5f784a1e860ea46507bf10b28703 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 25 Mar 2026 18:21:57 +0000 Subject: [PATCH] Expose AI settings action in Patrol runtime findings Expanded Patrol runtime findings now offer the same direct Open AI Settings action as the summary card, with the action owned by the shared finding presentation helper. --- .../subsystems/patrol-intelligence.md | 5 +++ .../src/components/AI/FindingsPanel.tsx | 19 +++++++++++ .../AI/__tests__/FindingsPanel.test.ts | 32 +++++++++++++++++++ .../src/utils/aiFindingPresentation.ts | 18 +++++++++++ 4 files changed, 74 insertions(+) diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 2522c0c52..964b49a7b 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -170,6 +170,11 @@ subject labels, so Patrol-owned synthetic service findings render as `Patrol runtime` rather than leaking backend resource internals like `Pulse Patrol Service (service)` into the primary findings row or assistant handoff prompts. +That same finding presentation contract should own the primary remediation path +for Patrol-owned runtime findings as well. Expanded runtime-finding rows should +offer the same direct `Open AI Settings` action that the top assessment uses, +instead of falling back to only generic acknowledge, snooze, or dismiss +controls. The summary recency chip must follow the same governed scope distinction. When the latest completed activity was only a scoped run, the summary should label that timestamp as `Last activity` instead of `Last patrol`; `Last full patrol` diff --git a/frontend-modern/src/components/AI/FindingsPanel.tsx b/frontend-modern/src/components/AI/FindingsPanel.tsx index 5bc839580..6b0befb61 100644 --- a/frontend-modern/src/components/AI/FindingsPanel.tsx +++ b/frontend-modern/src/components/AI/FindingsPanel.tsx @@ -43,6 +43,7 @@ import { getFindingStatusLabel, getFindingSourceBadgeClasses, getFindingSourceLabel, + getFindingPrimaryActionPresentation, getFindingSubjectPresentation, getPatrolFindingClassification, getFindingRecencyPresentation, @@ -664,6 +665,24 @@ export const FindingsPanel: Component = (props) => { // Render expanded content for a finding const renderExpandedContent = (finding: UnifiedFinding) => (
+ {(() => { + const primaryAction = getFindingPrimaryActionPresentation(finding); + return ( + + {(action) => ( + + )} + + ); + })()}
Triggered by alert{finding.alertType ? ` (${finding.alertType})` : ''} • Identifier{' '} diff --git a/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts b/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts index 9ea2e1048..3ac6b6d3d 100644 --- a/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts +++ b/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts @@ -6,6 +6,7 @@ import { formatFindingLifecycleType, formatFindingLoopState, getFindingEmptyStateCopy, + getFindingPrimaryActionPresentation, getFindingSubjectPresentation, getPatrolFindingClassification, getFindingSeverityCompactLabel, @@ -219,6 +220,31 @@ describe('aiFindingPresentation', () => { }); }); + describe('findingPrimaryActionPresentation', () => { + it('offers AI settings as the primary action for Patrol runtime findings', () => { + expect( + getFindingPrimaryActionPresentation({ + resourceId: 'ai-service', + resourceName: 'Pulse Patrol Service', + title: 'Pulse Patrol: Insufficient API credits', + }), + ).toEqual({ + label: 'Open AI Settings', + href: '/settings/system-ai', + }); + }); + + it('does not expose AI settings as the primary action for infrastructure findings', () => { + expect( + getFindingPrimaryActionPresentation({ + resourceId: 'vm-101', + resourceName: 'db-01', + title: 'Disk nearly full', + }), + ).toBeUndefined(); + }); + }); + describe('filterPresentation', () => { it('builds canonical filter options', () => { expect( @@ -268,6 +294,12 @@ describe('aiFindingPresentation', () => { expect(findingsPanelSource).not.toContain('AI-discovered insights'); }); + it('exposes the canonical primary action for Patrol runtime findings inside the expanded row', () => { + expect(findingsPanelSource).toContain('getFindingPrimaryActionPresentation'); + expect(findingsPanelSource).toContain('{action().label}'); + expect(findingsPanelSource).toContain("href={action().href}"); + }); + it('only shows the sort control when there are multiple Patrol findings to sort', () => { expect(findingsPanelSource).toContain(' 1}>'); expect(findingsPanelSource).toContain(''); diff --git a/frontend-modern/src/utils/aiFindingPresentation.ts b/frontend-modern/src/utils/aiFindingPresentation.ts index 1423739b9..3b43204c0 100644 --- a/frontend-modern/src/utils/aiFindingPresentation.ts +++ b/frontend-modern/src/utils/aiFindingPresentation.ts @@ -211,6 +211,11 @@ export interface FindingSubjectPresentation { label: string; } +export interface FindingPrimaryActionPresentation { + label: string; + href: string; +} + export const getFindingSourceLabel = (source: UnifiedFinding['source'] | string): string => FINDING_SOURCE_LABELS[source] || source; @@ -288,6 +293,19 @@ export const getFindingSubjectPresentation = ( }; }; +export const getFindingPrimaryActionPresentation = ( + finding: Pick, +): FindingPrimaryActionPresentation | undefined => { + if (isPatrolRuntimeFinding(finding)) { + return { + label: 'Open AI Settings', + href: '/settings/system-ai', + }; + } + + return undefined; +}; + export const getFindingRecencyPresentation = ( finding: Pick, ): FindingRecencyPresentation => {