mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 10:43:36 +00:00
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:
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user