Move Patrol investigation context behind findings

Patrol now renders investigation context beneath the findings and history workspace, keeping the primary summary card focused on assessment and verification while preserving the same context content and toggle behavior.
This commit is contained in:
rcourtman 2026-03-25 18:11:16 +00:00
parent dda60b3d21
commit 247bccded4
4 changed files with 66 additions and 62 deletions

View file

@ -192,6 +192,10 @@ operator whether Patrol recently completed a successful full patrol, only ran
scoped alert-triggered checks, or ended its most recent full patrol with
errors, so the page does not leave trust and coverage as implicit background
knowledge.
The same hierarchy applies to investigation context. Correlations, recent
changes, and policy posture are secondary evidence for deeper investigation, so
the `Investigation context` section belongs beneath the primary findings/history
workspace rather than inside the assessment card itself.
The Patrol status bar should stay factual and operational within that same
hierarchy. `frontend-modern/src/components/patrol/PatrolStatusBar.tsx` is a
recent-activity strip, not a second health verdict: when Patrol is active it

View file

@ -13,9 +13,6 @@ import {
} from '@/utils/patrolSummaryPresentation';
import { getPatrolRuntimePresentation } from '@/utils/patrolRuntimePresentation';
import { getSemanticTonePresentation } from '@/utils/semanticTonePresentation';
import { ResourcePolicySummary } from '@/components/Infrastructure/ResourcePolicySummary';
import { ResourceCorrelationSummary } from '@/components/Infrastructure/ResourceCorrelationSummary';
import { ResourceChangeSummary } from '@/components/Infrastructure/ResourceChangeSummary';
import { formatRelativeTime } from '@/utils/format';
import type { PatrolIntelligenceState } from './usePatrolIntelligenceState';
@ -190,66 +187,7 @@ export function PatrolIntelligenceSummary(props: { state: PatrolIntelligenceStat
</div>
</div>
</div>
</div>
<Show when={state.hasInvestigationContext()}>
<div class="mt-4 rounded-md border border-border-subtle bg-base/90 p-3">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted">
Investigation context
</p>
<p class="mt-1 text-sm text-muted">
Secondary change and policy signals for deeper investigation.
</p>
<Show when={state.investigationContextSummary()}>
<p class="mt-1 text-xs text-base-content">
{state.investigationContextSummary()}
</p>
</Show>
</div>
<button
type="button"
onClick={() => state.setShowInvestigationContext((value) => !value)}
class="inline-flex items-center rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover"
>
{state.showInvestigationContext() ? 'Hide context' : 'Show context'}
</button>
</div>
<Show when={state.showInvestigationContext()}>
<div class="mt-4 grid gap-4 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
<Show when={state.recentChangeCount() > 0}>
<ResourceChangeSummary
class="space-y-0"
title="Recent changes"
subtitle="Last 24 hours"
changes={summary().recent_changes}
maxChanges={3}
compact
/>
</Show>
<div class="space-y-4">
<Show when={state.correlations().length > 0}>
<ResourceCorrelationSummary
title="Correlations"
correlations={state.correlations()}
summaryText={`${state.correlationTotal()} total`}
/>
</Show>
<ResourcePolicySummary
posture={state.policyPosture()}
title="Policy posture"
/>
</div>
</div>
</Show>
</div>
</Show>
</section>
)}
</Show>

View file

@ -4,6 +4,9 @@ import { ApprovalBanner, PatrolStatusBar, RunHistoryPanel } from '@/components/p
import { getFindingSeverityToneClasses } from '@/utils/aiFindingPresentation';
import { formatRelativeTime } from '@/utils/format';
import { formatTriggerReason } from '@/utils/patrolFormat';
import { ResourcePolicySummary } from '@/components/Infrastructure/ResourcePolicySummary';
import { ResourceCorrelationSummary } from '@/components/Infrastructure/ResourceCorrelationSummary';
import { ResourceChangeSummary } from '@/components/Infrastructure/ResourceChangeSummary';
import type { PatrolIntelligenceState } from './usePatrolIntelligenceState';
export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceState }) {
@ -120,6 +123,59 @@ export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceSt
patrolStream={state.patrolStream}
/>
</Show>
<Show when={state.hasInvestigationContext()}>
<section class="rounded-md border border-border-subtle bg-base/90 p-3">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted">
Investigation context
</p>
<p class="mt-1 text-sm text-muted">
Secondary change and policy signals for deeper investigation.
</p>
<Show when={state.investigationContextSummary()}>
<p class="mt-1 text-xs text-base-content">{state.investigationContextSummary()}</p>
</Show>
</div>
<button
type="button"
onClick={() => state.setShowInvestigationContext((value) => !value)}
class="inline-flex items-center rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover"
>
{state.showInvestigationContext() ? 'Hide context' : 'Show context'}
</button>
</div>
<Show when={state.showInvestigationContext()}>
<div class="mt-4 grid gap-4 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
<Show when={state.recentChangeCount() > 0}>
<ResourceChangeSummary
class="space-y-0"
title="Recent changes"
subtitle="Last 24 hours"
changes={state.intelligenceSummary()?.recent_changes}
maxChanges={3}
compact
/>
</Show>
<div class="space-y-4">
<Show when={state.correlations().length > 0}>
<ResourceCorrelationSummary
title="Correlations"
correlations={state.correlations()}
summaryText={`${state.correlationTotal()} total`}
/>
</Show>
<ResourcePolicySummary posture={state.policyPosture()} title="Policy posture" />
</div>
</div>
</Show>
</section>
</Show>
</>
);
}

View file

@ -600,6 +600,12 @@ describe('AIIntelligence entitlement gating', () => {
expect(screen.getByText('No active issues detected')).toBeInTheDocument();
});
const findingsPanel = screen.getByTestId('findings-panel');
const contextHeading = screen.getByText('Investigation context');
expect(
Boolean(findingsPanel.compareDocumentPosition(contextHeading) & Node.DOCUMENT_POSITION_FOLLOWING),
).toBe(true);
expect(screen.getByText(/Health A · 91\/100/)).toBeInTheDocument();
expect(screen.getByText('Investigation context')).toBeInTheDocument();
expect(screen.getByText('1 recent change · 4 governed resources')).toBeInTheDocument();