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}> + + 0}>