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.
This commit is contained in:
rcourtman
2026-03-25 18:21:57 +00:00
parent d98687a8fc
commit 9a507885a3
4 changed files with 74 additions and 0 deletions
@@ -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`
@@ -43,6 +43,7 @@ import {
getFindingStatusLabel,
getFindingSourceBadgeClasses,
getFindingSourceLabel,
getFindingPrimaryActionPresentation,
getFindingSubjectPresentation,
getPatrolFindingClassification,
getFindingRecencyPresentation,
@@ -664,6 +665,24 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
// Render expanded content for a finding
const renderExpandedContent = (finding: UnifiedFinding) => (
<div class="mt-3 pt-3 border-t border-border-subtle">
{(() => {
const primaryAction = getFindingPrimaryActionPresentation(finding);
return (
<Show when={primaryAction}>
{(action) => (
<div class="mb-3">
<a
href={action().href}
onClick={(e) => e.stopPropagation()}
class="inline-flex items-center rounded border border-border bg-surface px-2.5 py-1.5 text-xs font-semibold text-base-content transition-colors hover:bg-surface-hover"
>
{action().label}
</a>
</div>
)}
</Show>
);
})()}
<Show when={hasTriggeringAlert(finding)}>
<div class="text-xs text-amber-700 dark:text-amber-300 mb-2">
Triggered by alert{finding.alertType ? ` (${finding.alertType})` : ''} Identifier{' '}
@@ -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('<Show when={patrolFindings().length > 1}>');
expect(findingsPanelSource).toContain('<option value="severity">By Severity</option>');
@@ -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<UnifiedFinding, 'resourceId' | 'resourceName' | 'title'>,
): FindingPrimaryActionPresentation | undefined => {
if (isPatrolRuntimeFinding(finding)) {
return {
label: 'Open AI Settings',
href: '/settings/system-ai',
};
}
return undefined;
};
export const getFindingRecencyPresentation = (
finding: Pick<UnifiedFinding, 'status' | 'detectedAt' | 'lastSeenAt'>,
): FindingRecencyPresentation => {