From fb980427353cf3f9b6e07950d044e2d9584378bb Mon Sep 17 00:00:00 2001
From: rcourtman
Date: Sun, 1 Feb 2026 10:12:39 +0000
Subject: [PATCH] feat(ui): enhance findings panel and investigation display
- Add timed_out, fix_verified, fix_verification_failed outcome types
- Fix field name mapping for investigation status from API
- Add findingsNeedingAttention computed property
- Update fixed count to include verified fixes
- Improve autonomy mode descriptions to mention verification
- Auto-switch to approvals filter when clicking approval banner
- Improve stats label for monitor vs investigating modes
---
frontend-modern/src/api/ai.ts | 14 +--
frontend-modern/src/api/patrol.ts | 20 +++-
.../src/components/AI/AIStatusIndicator.tsx | 8 ++
.../src/components/AI/FindingsPanel.tsx | 91 ++++++++++++++++---
.../src/components/patrol/ApprovalBanner.tsx | 11 ++-
.../src/components/patrol/ApprovalSection.tsx | 48 ++++++++--
.../patrol/InvestigationSection.tsx | 33 ++++---
frontend-modern/src/pages/AIIntelligence.tsx | 23 +++--
frontend-modern/src/stores/aiIntelligence.ts | 23 +++--
frontend-modern/src/types/aiIntelligence.ts | 2 +-
10 files changed, 214 insertions(+), 59 deletions(-)
diff --git a/frontend-modern/src/api/ai.ts b/frontend-modern/src/api/ai.ts
index 92913d222..b91dadb2e 100644
--- a/frontend-modern/src/api/ai.ts
+++ b/frontend-modern/src/api/ai.ts
@@ -342,12 +342,12 @@ export interface UnifiedFindingRecord {
ai_confidence?: number;
enhanced_by_ai?: boolean;
ai_enhanced_at?: string;
- // Investigation fields (camelCase from API)
- investigationSessionId?: string;
- investigationStatus?: string;
- investigationOutcome?: string;
- lastInvestigatedAt?: string;
- investigationAttempts?: number;
+ // Investigation fields (snake_case from Go backend)
+ investigation_session_id?: string;
+ investigation_status?: string;
+ investigation_outcome?: string;
+ last_investigated_at?: string;
+ investigation_attempts?: number;
detected_at: string;
last_seen_at?: string;
resolved_at?: string;
@@ -476,7 +476,7 @@ export interface ApprovalExecutionResult {
// ============================================
export type InvestigationStatus = 'pending' | 'running' | 'completed' | 'failed' | 'needs_attention';
-export type InvestigationOutcome = 'resolved' | 'fix_queued' | 'fix_executed' | 'fix_failed' | 'needs_attention' | 'cannot_fix';
+export type InvestigationOutcome = 'resolved' | 'fix_queued' | 'fix_executed' | 'fix_failed' | 'needs_attention' | 'cannot_fix' | 'timed_out' | 'fix_verified' | 'fix_verification_failed';
export interface ProposedFix {
id: string;
diff --git a/frontend-modern/src/api/patrol.ts b/frontend-modern/src/api/patrol.ts
index 57539e246..5f9cf2935 100644
--- a/frontend-modern/src/api/patrol.ts
+++ b/frontend-modern/src/api/patrol.ts
@@ -42,7 +42,7 @@ export interface Finding {
}
export type InvestigationStatus = 'pending' | 'running' | 'completed' | 'failed' | 'needs_attention';
-export type InvestigationOutcome = 'resolved' | 'fix_queued' | 'fix_executed' | 'fix_failed' | 'needs_attention' | 'cannot_fix';
+export type InvestigationOutcome = 'resolved' | 'fix_queued' | 'fix_executed' | 'fix_failed' | 'needs_attention' | 'cannot_fix' | 'timed_out' | 'fix_verified' | 'fix_verification_failed';
export type PatrolAutonomyLevel = 'monitor' | 'approval' | 'assisted' | 'full';
export interface PatrolAutonomySettings {
@@ -309,6 +309,24 @@ export const investigationOutcomeLabels: Record =
fix_failed: 'Fix Failed',
needs_attention: 'Needs Attention',
cannot_fix: 'Cannot Auto-Fix',
+ timed_out: 'Timed Out — Will Retry',
+ fix_verified: 'Fix Verified',
+ fix_verification_failed: 'Verification Failed',
+};
+
+/**
+ * Investigation outcome badge colors for UI
+ */
+export const investigationOutcomeColors: Record = {
+ resolved: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
+ fix_queued: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
+ fix_executed: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
+ fix_failed: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
+ needs_attention: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
+ cannot_fix: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400',
+ timed_out: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
+ fix_verified: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
+ fix_verification_failed: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
};
// =============================================================================
diff --git a/frontend-modern/src/components/AI/AIStatusIndicator.tsx b/frontend-modern/src/components/AI/AIStatusIndicator.tsx
index c4df937ef..68b501c8c 100644
--- a/frontend-modern/src/components/AI/AIStatusIndicator.tsx
+++ b/frontend-modern/src/components/AI/AIStatusIndicator.tsx
@@ -10,6 +10,7 @@ import { useNavigate } from '@solidjs/router';
import { getPatrolStatus, type PatrolStatus } from '../../api/patrol';
import { useAllAnomalies } from '@/hooks/useAnomalies';
import { useLearningStatus } from '@/hooks/useLearningStatus';
+import { aiIntelligenceStore } from '@/stores/aiIntelligence';
import './AIStatusIndicator.css';
@@ -53,6 +54,7 @@ export function AIStatusIndicator() {
const hasIssues = createMemo(() => {
const s = status();
+ if (aiIntelligenceStore.needsAttentionCount > 0 || aiIntelligenceStore.pendingApprovalCount > 0) return true;
if (!s) return false;
return s.summary.critical > 0 || s.summary.warning > 0;
});
@@ -101,6 +103,12 @@ export function AIStatusIndicator() {
if (s.summary.watch > 0) parts.push(`${s.summary.watch} watching`);
}
+ // Investigation status
+ const attentionCount = aiIntelligenceStore.needsAttentionCount;
+ const approvalCount = aiIntelligenceStore.pendingApprovalCount;
+ if (attentionCount > 0) parts.push(`${attentionCount} need${attentionCount === 1 ? 's' : ''} attention`);
+ if (approvalCount > 0) parts.push(`${approvalCount} awaiting approval`);
+
// Anomaly status
const counts = anomalyCounts();
const anomalyTotal = totalAnomalies();
diff --git a/frontend-modern/src/components/AI/FindingsPanel.tsx b/frontend-modern/src/components/AI/FindingsPanel.tsx
index deb7af951..45a2e524f 100644
--- a/frontend-modern/src/components/AI/FindingsPanel.tsx
+++ b/frontend-modern/src/components/AI/FindingsPanel.tsx
@@ -17,7 +17,7 @@ import { aiIntelligenceStore, type UnifiedFinding } from '@/stores/aiIntelligenc
import { notificationStore } from '@/stores/notifications';
import { aiChatStore } from '@/stores/aiChat';
import { InvestigationSection, ApprovalSection } from '@/components/patrol';
-import { investigationStatusLabels, investigationOutcomeLabels, type InvestigationStatus } from '@/api/patrol';
+import { investigationStatusLabels, investigationOutcomeLabels, investigationOutcomeColors, type InvestigationStatus } from '@/api/patrol';
import { AIAPI, type RemediationPlan } from '@/api/ai';
// Severity priority for sorting (lower number = higher priority)
@@ -70,7 +70,7 @@ interface FindingsPanelProps {
showResolved?: boolean;
maxItems?: number;
onFindingClick?: (finding: UnifiedFinding) => void;
- filterOverride?: 'all' | 'active' | 'resolved' | 'approvals';
+ filterOverride?: 'all' | 'active' | 'resolved' | 'approvals' | 'attention';
filterFindingIds?: string[];
showControls?: boolean;
nextPatrolAt?: string;
@@ -83,7 +83,7 @@ interface FindingsPanelProps {
export const FindingsPanel: Component = (props) => {
const location = useLocation();
- const [filter, setFilter] = createSignal<'all' | 'active' | 'resolved' | 'approvals'>(props.filterOverride ?? 'active');
+ const [filter, setFilter] = createSignal<'all' | 'active' | 'resolved' | 'approvals' | 'attention'>(props.filterOverride ?? 'active');
const [sortBy, setSortBy] = createSignal<'severity' | 'time'>('severity');
const [expandedId, setExpandedId] = createSignal(null);
const [actionLoading, setActionLoading] = createSignal(null);
@@ -154,6 +154,16 @@ export const FindingsPanel: Component = (props) => {
}
});
+ // Auto-reset filter when the conditional filter buttons disappear
+ createEffect(() => {
+ if (filter() === 'attention' && aiIntelligenceStore.needsAttentionCount === 0) {
+ setFilter('active');
+ }
+ if (filter() === 'approvals' && aiIntelligenceStore.pendingApprovalCount === 0) {
+ setFilter('active');
+ }
+ });
+
// Filter and sort findings
const filteredFindings = createMemo(() => {
let findings = [...aiIntelligenceStore.findings];
@@ -168,6 +178,9 @@ export const FindingsPanel: Component = (props) => {
findings = findings.filter(f => f.status === 'active');
} else if (filter() === 'resolved') {
findings = findings.filter(f => f.status === 'resolved' || f.status === 'dismissed');
+ } else if (filter() === 'attention') {
+ const attentionIds = new Set(aiIntelligenceStore.findingsNeedingAttention.map(f => f.id));
+ findings = findings.filter(f => attentionIds.has(f.id));
} else if (filter() === 'approvals') {
const approvalFindingIds = new Set(aiIntelligenceStore.findingsWithPendingApprovals.map(f => f.id));
findings = findings.filter(f => approvalFindingIds.has(f.id));
@@ -179,8 +192,21 @@ export const FindingsPanel: Component = (props) => {
findings = findings.filter(f => idSet.has(f.id));
}
+ // Outcome priority: findings needing attention sort first
+ const outcomeOrder: Record = {
+ fix_verification_failed: 0,
+ timed_out: 1,
+ needs_attention: 1,
+ fix_queued: 2,
+ };
+
// Sort
findings.sort((a, b) => {
+ // Active findings with actionable outcomes sort above others
+ const aOutcome = (a.status === 'active' && a.investigationOutcome) ? (outcomeOrder[a.investigationOutcome] ?? 3) : 3;
+ const bOutcome = (b.status === 'active' && b.investigationOutcome) ? (outcomeOrder[b.investigationOutcome] ?? 3) : 3;
+ if (aOutcome !== bOutcome) return aOutcome - bOutcome;
+
if (sortBy() === 'severity') {
const aPriority = severityOrder[a.severity] ?? 4;
const bPriority = severityOrder[b.severity] ?? 4;
@@ -362,7 +388,22 @@ export const FindingsPanel: Component = (props) => {
}
if (finding.source === 'ai-patrol') {
- return `Issue no longer detected ${resolvedTime}`;
+ switch (finding.investigationOutcome) {
+ case 'fix_verified':
+ return `Fixed by Patrol ${resolvedTime}`;
+ case 'fix_executed':
+ return `Fix applied by Patrol ${resolvedTime}`;
+ case 'resolved':
+ return `Resolved by Patrol ${resolvedTime}`;
+ case 'fix_verification_failed':
+ return `Resolved after failed verification ${resolvedTime}`;
+ case 'timed_out':
+ return `Resolved after investigation timeout ${resolvedTime}`;
+ case 'cannot_fix':
+ return `Resolved manually ${resolvedTime}`;
+ default:
+ return `Issue no longer detected ${resolvedTime}`;
+ }
}
return `Resolved ${resolvedTime}`;
@@ -427,8 +468,8 @@ export const FindingsPanel: Component = (props) => {
Out of scope
- {/* Investigation status badge */}
-
+ {/* Investigation status badge — only when no outcome badge will show */}
+
= (props) => {
{investigationStatusLabels[finding.investigationStatus!]}
- {/* Investigation outcome badge */}
-
-
+ {/* Investigation outcome badge — replaces status badge when outcome is known */}
+
+
{investigationOutcomeLabels[finding.investigationOutcome!]}
@@ -467,6 +508,11 @@ export const FindingsPanel: Component = (props) => {
({finding.dismissedReason?.replace(/_/g, ' ')})
+
+
+ last investigated {formatTime(finding.lastInvestigatedAt!)}
+
+
{/* Actions */}
@@ -688,6 +734,7 @@ export const FindingsPanel: Component = (props) => {
findingId={finding.id}
investigationStatus={finding.investigationStatus}
investigationOutcome={finding.investigationOutcome}
+ investigationAttempts={finding.investigationAttempts}
/>
@@ -695,7 +742,9 @@ export const FindingsPanel: Component = (props) => {
= (props) => {
class={`px-2 py-1 border ${filter() === 'resolved'
? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 border-blue-300 dark:border-blue-700'
: 'border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700'
- } ${aiIntelligenceStore.pendingApprovalCount > 0 ? '' : 'rounded-r'}`}
+ } ${aiIntelligenceStore.needsAttentionCount > 0 || aiIntelligenceStore.pendingApprovalCount > 0 ? '' : 'rounded-r'}`}
>
Resolved
+ 0}>
+ setFilter('attention')}
+ class={`px-2 py-1 border ${filter() === 'attention'
+ ? 'bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-700'
+ : 'border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700'
+ } ${aiIntelligenceStore.pendingApprovalCount > 0 ? '' : 'rounded-r'}`}
+ >
+ Needs Attention ({aiIntelligenceStore.needsAttentionCount})
+
+
0}>
= (props) => {
-
+
+ No findings need attention right now.
+
+
+ No pending approvals.
+
+
No Patrol findings to display
diff --git a/frontend-modern/src/components/patrol/ApprovalBanner.tsx b/frontend-modern/src/components/patrol/ApprovalBanner.tsx
index 443591b65..7bca248a9 100644
--- a/frontend-modern/src/components/patrol/ApprovalBanner.tsx
+++ b/frontend-modern/src/components/patrol/ApprovalBanner.tsx
@@ -8,14 +8,13 @@
* - Multiple: count + "Review" button to scroll to first finding
*/
-import { Component, Show, createMemo } from 'solid-js';
+import { Component, Show, createMemo, createSignal, onCleanup } from 'solid-js';
import { aiIntelligenceStore } from '@/stores/aiIntelligence';
import { notificationStore } from '@/stores/notifications';
import type { ApprovalRequest } from '@/api/ai';
import ShieldAlertIcon from 'lucide-solid/icons/shield-alert';
import CheckIcon from 'lucide-solid/icons/check';
import XIcon from 'lucide-solid/icons/x';
-import { createSignal } from 'solid-js';
interface ApprovalBannerProps {
onScrollToFinding?: (findingId: string) => void;
@@ -23,6 +22,11 @@ interface ApprovalBannerProps {
export const ApprovalBanner: Component = (props) => {
const [actionLoading, setActionLoading] = createSignal(null);
+ const [tick, setTick] = createSignal(Date.now());
+
+ // Tick every second to keep countdown live
+ const tickInterval = setInterval(() => setTick(Date.now()), 1000);
+ onCleanup(() => clearInterval(tickInterval));
const pending = createMemo(() =>
aiIntelligenceStore.pendingApprovals.filter((a: ApprovalRequest) => a.status === 'pending')
@@ -31,6 +35,7 @@ export const ApprovalBanner: Component = (props) => {
const firstApproval = createMemo(() => pending()[0] ?? null);
const timeRemaining = (expiresAt: string) => {
+ void tick(); // subscribe to tick signal for reactivity
const diff = new Date(expiresAt).getTime() - Date.now();
if (diff <= 0) return 'expired';
const mins = Math.floor(diff / 60000);
@@ -103,7 +108,7 @@ export const ApprovalBanner: Component = (props) => {
expires {timeRemaining(firstApproval()!.expiresAt)}
-
+
{firstApproval()!.context}
diff --git a/frontend-modern/src/components/patrol/ApprovalSection.tsx b/frontend-modern/src/components/patrol/ApprovalSection.tsx
index 33645843c..ba0eca47c 100644
--- a/frontend-modern/src/components/patrol/ApprovalSection.tsx
+++ b/frontend-modern/src/components/patrol/ApprovalSection.tsx
@@ -2,7 +2,7 @@
* ApprovalSection
*
* Shows when investigation has a proposed fix.
- * States: Pending, Expired, Executed, Denied, Failed.
+ * States: Pending, Expired, Executed, Denied, Failed, Verified, VerificationFailed.
*/
import { Component, Show, createSignal, createResource, createMemo } from 'solid-js';
@@ -32,7 +32,7 @@ export const ApprovalSection: Component = (props) => {
() => props.findingId,
async (findingId) => {
// Only load if outcome indicates a fix was queued but no pending approval
- if (props.investigationOutcome !== 'fix_queued' && props.investigationOutcome !== 'fix_executed' && props.investigationOutcome !== 'fix_failed') {
+ if (props.investigationOutcome !== 'fix_queued' && props.investigationOutcome !== 'fix_executed' && props.investigationOutcome !== 'fix_failed' && props.investigationOutcome !== 'fix_verified' && props.investigationOutcome !== 'fix_verification_failed') {
return null;
}
try {
@@ -51,11 +51,11 @@ export const ApprovalSection: Component = (props) => {
);
const isExecuted = createMemo(() =>
- props.investigationOutcome === 'fix_executed' || executionResult()?.success
+ props.investigationOutcome === 'fix_executed' || props.investigationOutcome === 'fix_verified' || executionResult()?.success
);
const isFailed = createMemo(() =>
- props.investigationOutcome === 'fix_failed' || (executionResult() && !executionResult()!.success)
+ props.investigationOutcome === 'fix_failed' || props.investigationOutcome === 'fix_verification_failed' || (executionResult() && !executionResult()!.success)
);
// Show section only when there's something to display
@@ -243,8 +243,26 @@ export const ApprovalSection: Component = (props) => {
- Fix executed successfully
+ {props.investigationOutcome === 'fix_verified' ? 'Fix verified — issue resolved' : 'Fix executed successfully'}
+
+ {(fix) => (
+
+
{fix().description}
+
0}>
+
+ {fix().commands![0]}
+
+
+
+ Target: {fix().target_host}
+
+
+ {fix().rationale}
+
+
+ )}
+
{/* Failed (from backend state, no local result) */}
@@ -253,8 +271,26 @@ export const ApprovalSection: Component = (props) => {
- Fix execution failed
+ {props.investigationOutcome === 'fix_verification_failed' ? 'Fix executed but issue persists' : 'Fix execution failed'}
+
+ {(fix) => (
+
+
{fix().description}
+
0}>
+
+ {fix().commands![0]}
+
+
+
+ Target: {fix().target_host}
+
+
+ {fix().rationale}
+
+
+ )}
+
diff --git a/frontend-modern/src/components/patrol/InvestigationSection.tsx b/frontend-modern/src/components/patrol/InvestigationSection.tsx
index 64d028ed2..f788485a7 100644
--- a/frontend-modern/src/components/patrol/InvestigationSection.tsx
+++ b/frontend-modern/src/components/patrol/InvestigationSection.tsx
@@ -12,6 +12,7 @@ import {
reinvestigateFinding,
investigationStatusLabels,
investigationOutcomeLabels,
+ investigationOutcomeColors,
formatTimestamp,
} from '@/api/patrol';
import { InvestigationMessages } from './InvestigationMessages';
@@ -23,6 +24,7 @@ interface InvestigationSectionProps {
findingId: string;
investigationStatus?: string;
investigationOutcome?: string;
+ investigationAttempts?: number;
}
const statusColors: Record = {
@@ -33,14 +35,6 @@ const statusColors: Record = {
needs_attention: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
};
-const outcomeColors: Record = {
- resolved: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
- fix_queued: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
- fix_executed: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300',
- fix_failed: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300',
- needs_attention: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
- cannot_fix: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400',
-};
export const InvestigationSection: Component = (props) => {
const [showThread, setShowThread] = createSignal(false);
@@ -74,7 +68,7 @@ export const InvestigationSection: Component = (props
const inv = investigation();
if (!inv) return true; // No investigation yet
if (inv.status === 'running') return false;
- return inv.status === 'failed' || inv.status === 'needs_attention' || inv.outcome === 'cannot_fix';
+ return inv.status === 'failed' || inv.status === 'needs_attention' || inv.outcome === 'cannot_fix' || inv.outcome === 'timed_out' || inv.outcome === 'fix_verification_failed';
};
const handleReinvestigate = async (e: Event) => {
@@ -99,14 +93,23 @@ export const InvestigationSection: Component = (props
Investigation
-
-
- {investigationStatusLabels[investigation()!.status] || investigation()!.status}
+ {/* Show outcome badge when available, otherwise show status badge */}
+
+
+ {investigationStatusLabels[investigation()!.status] || investigation()!.status}
+
+
+ }
+ >
+
+ {investigationOutcomeLabels[investigation()!.outcome!] || investigation()!.outcome}
-
-
- {investigationOutcomeLabels[investigation()!.outcome!] || investigation()!.outcome}
+ 1}>
+
+ attempt {props.investigationAttempts}
diff --git a/frontend-modern/src/pages/AIIntelligence.tsx b/frontend-modern/src/pages/AIIntelligence.tsx
index 9a3444b91..e3d5050b8 100644
--- a/frontend-modern/src/pages/AIIntelligence.tsx
+++ b/frontend-modern/src/pages/AIIntelligence.tsx
@@ -87,6 +87,7 @@ type PatrolTab = 'findings' | 'history';
export function AIIntelligence() {
const [activeTab, setActiveTab] = createSignal
('findings');
+ const [findingsFilterOverride, setFindingsFilterOverride] = createSignal<'all' | 'active' | 'resolved' | 'approvals' | 'attention' | undefined>(undefined);
const [isRefreshing, setIsRefreshing] = createSignal(false);
const [autonomyLevel, setAutonomyLevel] = createSignal('monitor');
const [isUpdatingAutonomy, setIsUpdatingAutonomy] = createSignal(false);
@@ -638,7 +639,11 @@ export function AIIntelligence() {
const infoCount = activeFindings.filter(f => f.severity === 'info').length;
const investigatingCount = patrolFindings.filter(f => f.investigationStatus === 'running').length;
const totalActive = activeFindings.length;
- const fixedCount = resolvedFindings.length;
+ const fixedCount = patrolFindings.filter(f =>
+ f.investigationOutcome === 'fix_verified' ||
+ f.investigationOutcome === 'fix_executed' ||
+ f.investigationOutcome === 'resolved'
+ ).length;
return {
criticalFindings: criticalCount,
@@ -804,15 +809,15 @@ export function AIIntelligence() {
Approval
-
Patrol investigates findings. All fixes require your approval.
+
Patrol investigates findings. All fixes require your approval. Verifies fixes post-execution.
Assisted
-
Auto-fix warnings. Critical findings still need approval.
+
Auto-fix warnings and verify they worked. Critical findings still need approval.
Full
-
Auto-fix everything, including critical. Must be enabled in ⚙️ settings first.
+
Auto-fix everything, including critical, and verify results. Must be enabled in ⚙️ settings first.
@@ -1101,6 +1106,7 @@ export function AIIntelligence() {
are crossed, findings are created automatically. In Approval , Assisted , or Full mode,
Pulse Patrol investigates these findings - querying nodes, checking logs, and running diagnostics to
identify root causes. It then suggests fixes (Approval), applies safe fixes (Assisted), or applies all fixes (Full).
+ After a fix is applied, Patrol verifies the issue is actually resolved.
This is experimental. In Assisted mode, critical findings still require approval. Full mode (requires unlock in ⚙️) auto-fixes everything.
@@ -1124,7 +1130,8 @@ export function AIIntelligence() {
{
setActiveTab('findings');
- // Scroll to the finding after tab switch
+ setFindingsFilterOverride('approvals');
+ // Scroll to the finding after tab switch and filter change
requestAnimationFrame(() => {
const el = document.getElementById(`finding-${findingId}`);
el?.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -1210,7 +1217,7 @@ export function AIIntelligence() {
- {autonomyLevel() === 'monitor' ? 'Investigating' : 'Investigating'}
+ {autonomyLevel() === 'monitor' ? 'Discovering' : 'Investigating'}
0
@@ -1306,7 +1313,7 @@ export function AIIntelligence() {
setActiveTab('history')}
+ onClick={() => { setActiveTab('history'); setFindingsFilterOverride(undefined); }}
class={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
activeTab() === 'history'
? 'border-blue-500 text-blue-600 dark:text-blue-400'
@@ -1345,7 +1352,7 @@ export function AIIntelligence() {
nextPatrolAt={patrolStatus()?.next_patrol_at}
lastPatrolAt={patrolStatus()?.last_patrol_at}
patrolIntervalMs={patrolStatus()?.interval_ms}
- filterOverride={selectedRunFindingIds() ? 'all' : undefined}
+ filterOverride={selectedRunFindingIds() ? 'all' : findingsFilterOverride()}
filterFindingIds={selectedRunFindingIds() ?? undefined}
scopeResourceIds={selectedRun()?.scope_resource_ids}
scopeResourceTypes={selectedRun()?.scope_resource_types}
diff --git a/frontend-modern/src/stores/aiIntelligence.ts b/frontend-modern/src/stores/aiIntelligence.ts
index 17df79b2b..b6e7f8415 100644
--- a/frontend-modern/src/stores/aiIntelligence.ts
+++ b/frontend-modern/src/stores/aiIntelligence.ts
@@ -49,7 +49,7 @@ export interface UnifiedFinding {
// Investigation fields (Patrol Autonomy)
investigationSessionId?: string;
investigationStatus?: 'pending' | 'running' | 'completed' | 'failed' | 'needs_attention';
- investigationOutcome?: 'resolved' | 'fix_queued' | 'fix_executed' | 'fix_failed' | 'needs_attention' | 'cannot_fix';
+ investigationOutcome?: 'resolved' | 'fix_queued' | 'fix_executed' | 'fix_failed' | 'needs_attention' | 'cannot_fix' | 'timed_out' | 'fix_verified' | 'fix_verification_failed';
lastInvestigatedAt?: string;
investigationAttempts?: number;
}
@@ -131,11 +131,11 @@ export const aiIntelligenceStore = {
status,
correlatedFindingIds: item.correlated_ids,
remediationPlanId: item.remediation_id,
- investigationSessionId: item.investigationSessionId || '',
- investigationStatus: item.investigationStatus as UnifiedFinding['investigationStatus'],
- investigationOutcome: item.investigationOutcome as UnifiedFinding['investigationOutcome'],
- lastInvestigatedAt: item.lastInvestigatedAt || undefined,
- investigationAttempts: item.investigationAttempts || 0,
+ investigationSessionId: item.investigation_session_id || '',
+ investigationStatus: item.investigation_status as UnifiedFinding['investigationStatus'],
+ investigationOutcome: item.investigation_outcome as UnifiedFinding['investigationOutcome'],
+ lastInvestigatedAt: item.last_investigated_at || undefined,
+ investigationAttempts: item.investigation_attempts || 0,
};
});
@@ -263,6 +263,17 @@ export const aiIntelligenceStore = {
return unifiedFindings().filter(f => findingIds.has(f.id));
},
+ get findingsNeedingAttention() {
+ const actionableOutcomes = new Set(['fix_verification_failed', 'timed_out', 'needs_attention', 'cannot_fix']);
+ return unifiedFindings().filter(f =>
+ f.status === 'active' && f.investigationOutcome && actionableOutcomes.has(f.investigationOutcome)
+ );
+ },
+
+ get needsAttentionCount() {
+ return this.findingsNeedingAttention.length;
+ },
+
async loadPendingApprovals() {
try {
const approvals = await AIAPI.getPendingApprovals();
diff --git a/frontend-modern/src/types/aiIntelligence.ts b/frontend-modern/src/types/aiIntelligence.ts
index 17df79b2b..7d9c63025 100644
--- a/frontend-modern/src/types/aiIntelligence.ts
+++ b/frontend-modern/src/types/aiIntelligence.ts
@@ -49,7 +49,7 @@ export interface UnifiedFinding {
// Investigation fields (Patrol Autonomy)
investigationSessionId?: string;
investigationStatus?: 'pending' | 'running' | 'completed' | 'failed' | 'needs_attention';
- investigationOutcome?: 'resolved' | 'fix_queued' | 'fix_executed' | 'fix_failed' | 'needs_attention' | 'cannot_fix';
+ investigationOutcome?: 'resolved' | 'fix_queued' | 'fix_executed' | 'fix_failed' | 'needs_attention' | 'cannot_fix' | 'timed_out' | 'fix_verified' | 'fix_verification_failed';
lastInvestigatedAt?: string;
investigationAttempts?: number;
}