mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-22 03:02:35 +00:00
Add Create rule from this button on Patrol findings
Last of the seven contextual entries from the captured Pulse
Intelligence rubric. "Remember as expected" handles one
instance; "Create rule from this" promotes the pattern: any
future finding on the same {resource, category} pair auto-
dismisses inside the backend's existing
FindingsStore.isSuppressedInternal /
MatchesSuppressionRule machinery rather than surfacing as a new
finding.
The backend endpoint already exists
(POST /api/ai/patrol/suppressions →
HandleAddSuppressionRule → FindingsStore.AddSuppressionRule).
The button + inline confirm panel is the missing surface:
- frontend-modern/src/api/patrol.ts gains
createSuppressionRuleFromFinding(input) that POSTs to the
existing endpoint with the finding's resource + category +
operator-supplied reason.
- FindingsPanel adds a Create rule button at the end of the
lifecycle action row, plus an inline confirmation that
surfaces the rule scope (resource + category), requires a
reason, and explains the future-auto-dismiss commitment.
Submission goes through aiIntelligenceStore.loadDashboardData
so the local view reflects the audit trail of record.
- Mirrors the visual pattern of the existing dismiss-confirmation
panel but uses neutral surface styling because this isn't a
dismissal — it's a permanent commitment, distinct from
Remember as expected which dismisses just this instance.
No backend changes; the rule machinery and the API endpoint are
unchanged. This is the surface piece. Type-check clean.
This commit is contained in:
parent
5a7fde7b39
commit
f13893b63f
2 changed files with 146 additions and 0 deletions
|
|
@ -308,6 +308,40 @@ export async function dismissFinding(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a permanent suppression rule from a finding.
|
||||
*
|
||||
* Unlike dismissFinding (which acts on a single instance), this records a
|
||||
* durable pattern: future findings on the same {resource, category}
|
||||
* pair will be auto-dismissed by the backend's
|
||||
* isSuppressedInternal/MatchesSuppressionRule machinery. Used by the
|
||||
* per-finding "Create rule from this" button so an operator who has
|
||||
* been silencing the same noisy pattern repeatedly can promote that
|
||||
* judgment into a remembered rule rather than dismissing every recurrence.
|
||||
*
|
||||
* The description is required by the backend — it's the operator's
|
||||
* stated reason, surfaced when the rule is later listed or audited.
|
||||
* Pass an empty resourceId or category to broaden the rule's scope
|
||||
* (e.g. all categories on this resource, or this category on any
|
||||
* resource).
|
||||
*/
|
||||
export async function createSuppressionRuleFromFinding(input: {
|
||||
resourceId: string;
|
||||
resourceName: string;
|
||||
category: string;
|
||||
description: string;
|
||||
}): Promise<{ success: boolean; message: string; rule: { id: string } }> {
|
||||
return apiFetchJSON('/api/ai/patrol/suppressions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
resource_id: input.resourceId,
|
||||
resource_name: input.resourceName,
|
||||
category: input.category,
|
||||
description: input.description,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or update a user note on a finding
|
||||
* Notes provide context that Patrol sees on future runs.
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
import { useResources } from '@/hooks/useResources';
|
||||
import { InvestigationSection, ApprovalSection } from '@/components/patrol';
|
||||
import { AIAPI, type ApprovalRequest, type RemediationPlan } from '@/api/ai';
|
||||
import { createSuppressionRuleFromFinding } from '@/api/patrol';
|
||||
import type { PatrolRunRecord, PatrolRuntimeState } from '@/api/patrol';
|
||||
import { buildResolvedResourceSurfaceLinks } from '@/routing/resourceLinks';
|
||||
import { getApprovalRiskPresentation } from '@/utils/approvalRiskPresentation';
|
||||
|
|
@ -635,6 +636,59 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
|||
}
|
||||
};
|
||||
|
||||
// Create rule from this is the promotion path: take the operator's
|
||||
// implicit pattern (silencing the same {resource, category} pair
|
||||
// repeatedly) and turn it into a durable suppression rule the backend
|
||||
// remembers. After creation, future findings matching the rule
|
||||
// auto-dismiss inside FindingsStore.isSuppressedInternal rather than
|
||||
// re-surfacing every Patrol run. The button confirms the scope and
|
||||
// requires a reason so the rule has audit context.
|
||||
const [creatingRuleForId, setCreatingRuleForId] = createSignal<string | null>(null);
|
||||
const [createRuleDescription, setCreateRuleDescription] = createSignal('');
|
||||
|
||||
const handleStartCreateRule = (finding: UnifiedFinding, e: Event) => {
|
||||
e.stopPropagation();
|
||||
setCreatingRuleForId(finding.id);
|
||||
setCreateRuleDescription(finding.userNote || '');
|
||||
setExpandedId(finding.id);
|
||||
};
|
||||
|
||||
const handleCancelCreateRule = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
setCreatingRuleForId(null);
|
||||
};
|
||||
|
||||
const handleConfirmCreateRule = async (finding: UnifiedFinding, e: Event) => {
|
||||
e.stopPropagation();
|
||||
const description = createRuleDescription().trim();
|
||||
if (!description) {
|
||||
notificationStore.error('A reason for the rule is required');
|
||||
return;
|
||||
}
|
||||
setActionLoading(finding.id);
|
||||
try {
|
||||
await createSuppressionRuleFromFinding({
|
||||
resourceId: finding.resourceId,
|
||||
resourceName: finding.resourceName,
|
||||
category: finding.category || '',
|
||||
description,
|
||||
});
|
||||
notificationStore.success(
|
||||
`Rule created: future ${finding.category || 'matching'} findings on ${finding.resourceName} will auto-dismiss`,
|
||||
);
|
||||
setCreatingRuleForId(null);
|
||||
// Refresh so the operator sees the finding update (typically the
|
||||
// rule takes effect on next Patrol cycle, but the local view
|
||||
// should reflect the audit-trail-of-record).
|
||||
void aiIntelligenceStore.loadDashboardData();
|
||||
} catch (err) {
|
||||
console.error('Failed to create suppression rule:', err);
|
||||
notificationStore.error('Failed to create suppression rule');
|
||||
} finally {
|
||||
setActionLoading(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Copy a Markdown summary of the finding to the clipboard so the operator
|
||||
// can paste it into a chat, ticket, or incident channel. The shape mirrors
|
||||
// the seven-question schema (title + impact + recommendation + trust
|
||||
|
|
@ -1400,10 +1454,68 @@ export const FindingsPanel: Component<FindingsPanelProps> = (props) => {
|
|||
>
|
||||
Dismiss: Later
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleStartCreateRule(finding, e)}
|
||||
class="px-2 py-1 rounded border border-border hover:bg-surface-hover"
|
||||
disabled={actionLoading() === finding.id}
|
||||
title="Promote this dismissal into a permanent rule — future findings on this resource for this category will auto-dismiss without surfacing"
|
||||
>
|
||||
Create rule from this
|
||||
</button>
|
||||
</>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
{/* Inline create-rule confirmation. Confirms scope (resource +
|
||||
category) and requires a reason so the persisted rule has
|
||||
audit context. Mirrors the dismiss-confirmation panel
|
||||
visually but uses neutral surface styling — this isn't a
|
||||
dismissal, it's a permanent commitment. */}
|
||||
<Show when={creatingRuleForId() === finding.id}>
|
||||
<div class="mt-2 p-2 rounded border border-border bg-surface-alt">
|
||||
<div class="flex items-center gap-2 mb-1.5">
|
||||
<span class="text-xs font-medium text-base-content">
|
||||
Create suppression rule for{' '}
|
||||
<span class="font-semibold">{finding.resourceName}</span>
|
||||
<Show when={finding.category}>
|
||||
{' '}({finding.category})
|
||||
</Show>
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-[11px] text-muted mb-1.5">
|
||||
Future findings matching this resource and category will be
|
||||
auto-dismissed by Patrol without surfacing as new findings.
|
||||
You can list or remove rules later from the suppressions
|
||||
management surface.
|
||||
</p>
|
||||
<textarea
|
||||
class="w-full text-xs px-2 py-1.5 rounded border border-border bg-surface text-base-content resize-none focus:outline-none focus:ring-1 focus:ring-blue-400"
|
||||
rows={2}
|
||||
value={createRuleDescription()}
|
||||
onInput={(e) => setCreateRuleDescription(e.currentTarget.value)}
|
||||
placeholder="Why this rule? (required — e.g. 'delly backups are intentionally off-site, ignore failures')"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<div class="flex gap-2 mt-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => handleConfirmCreateRule(finding, e)}
|
||||
class="px-3 py-1 text-xs font-medium rounded bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50"
|
||||
disabled={actionLoading() === finding.id || !createRuleDescription().trim()}
|
||||
>
|
||||
Create rule
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCancelCreateRule}
|
||||
class="px-3 py-1 text-xs font-medium rounded border border-border hover:bg-surface-hover"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
{/* Inline dismiss confirmation. The header verb tracks the
|
||||
intent: "Remembering as" for expected_behavior (future-looking,
|
||||
"Pulse should know this is expected"), "Dismiss as" for the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue