From dee01cb85fb32ef143e45c65d533a60ebf94bb98 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 25 Mar 2026 21:41:59 +0000 Subject: [PATCH] Canonicalize dashboard attention ordering Move Patrol attention ordering into aiIntelligenceStore so dashboard hooks consume a canonical queue instead of adding their own ordering logic downstream. --- .../__tests__/useDashboardActions.test.ts | 64 ++++++++++++++----- .../src/hooks/useDashboardActions.ts | 3 +- .../stores/__tests__/aiIntelligence.test.ts | 47 ++++++++++++++ frontend-modern/src/stores/aiIntelligence.ts | 5 +- 4 files changed, 99 insertions(+), 20 deletions(-) diff --git a/frontend-modern/src/hooks/__tests__/useDashboardActions.test.ts b/frontend-modern/src/hooks/__tests__/useDashboardActions.test.ts index 16aad1202..f97ad8800 100644 --- a/frontend-modern/src/hooks/__tests__/useDashboardActions.test.ts +++ b/frontend-modern/src/hooks/__tests__/useDashboardActions.test.ts @@ -31,7 +31,6 @@ vi.mock('@/utils/logger', () => ({ import { AIAPI } from '@/api/ai'; import { useDashboardActions } from '@/hooks/useDashboardActions'; -import { sortFindingsForAttentionQueue } from '@/utils/aiFindingPresentation'; describe('useDashboardActions', () => { beforeEach(() => { @@ -109,29 +108,60 @@ describe('useDashboardActions', () => { dispose(); }); - it('prioritizes Patrol runtime findings in the dashboard attention queue', () => { - expect( - sortFindingsForAttentionQueue([ + it('surfaces the store-owned attention ordering unchanged', async () => { + vi.mocked(AIAPI.getUnifiedFindings).mockResolvedValue({ + findings: [ { id: 'infra-warning', - status: 'active', + source: 'ai-patrol', severity: 'warning', - resourceId: 'vm-101', - resourceName: 'db-01', + category: 'infrastructure', + resource_id: 'instance:node:101', + resource_name: 'db-01', + resource_type: 'host', title: 'Disk nearly full', - detectedAt: '2026-03-01T00:00:00Z', - } as never, + description: 'Storage usage is high.', + detected_at: '2026-03-01T00:00:00Z', + status: 'active', + investigation_outcome: 'fix_verification_unknown', + }, { id: 'runtime-warning', - status: 'active', + source: 'ai-patrol', severity: 'warning', - resourceId: 'ai-service', - resourceName: 'Pulse Patrol Service', + category: 'service', + resource_id: 'ai-service', + resource_name: 'Pulse Patrol Service', + resource_type: 'service', title: 'Pulse Patrol: Insufficient API credits', - lastSeenAt: '2026-03-01T00:05:00Z', - detectedAt: '2026-03-01T00:01:00Z', - } as never, - ]).map((finding) => finding.id), - ).toEqual(['runtime-warning', 'infra-warning']); + description: 'Provider credits are exhausted.', + detected_at: '2026-03-01T00:01:00Z', + last_seen_at: '2026-03-01T00:05:00Z', + status: 'active', + investigation_outcome: 'fix_failed', + }, + ], + count: 2, + } as never); + vi.mocked(AIAPI.getPendingApprovals).mockResolvedValue([] as never); + + let dispose!: () => void; + let actions!: ReturnType; + + createRoot((d) => { + dispose = d; + const [alertsList] = createSignal([]); + actions = useDashboardActions(alertsList); + }); + + await Promise.resolve(); + await Promise.resolve(); + + expect(actions.findingsNeedingAttention().map((finding) => finding.id)).toEqual([ + 'runtime-warning', + 'infra-warning', + ]); + + dispose(); }); }); diff --git a/frontend-modern/src/hooks/useDashboardActions.ts b/frontend-modern/src/hooks/useDashboardActions.ts index 30f23d2f8..4162f2885 100644 --- a/frontend-modern/src/hooks/useDashboardActions.ts +++ b/frontend-modern/src/hooks/useDashboardActions.ts @@ -4,7 +4,6 @@ import type { UnifiedFinding } from '@/stores/aiIntelligence'; import { aiIntelligenceStore } from '@/stores/aiIntelligence'; import type { ApprovalRequest } from '@/api/ai'; import { hasFeature } from '@/stores/license'; -import { sortFindingsForAttentionQueue } from '@/utils/aiFindingPresentation'; export interface DashboardActions { pendingApprovals: Accessor; @@ -55,7 +54,7 @@ export function useDashboardActions(alertsList: Accessor): DashboardAct const findingsNeedingAttention = createMemo(() => { if (!hasPatrol()) return []; - return sortFindingsForAttentionQueue(aiIntelligenceStore.findingsNeedingAttention); + return aiIntelligenceStore.findingsNeedingAttention; }); const totalActionCount = createMemo( diff --git a/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts b/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts index 378b0cd44..a0fb913dc 100644 --- a/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts +++ b/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts @@ -427,4 +427,51 @@ describe('aiIntelligenceStore', () => { await aiIntelligenceStore.loadPendingApprovals(); vi.useRealTimers(); }); + + it('sorts Patrol runtime findings to the top of the shared needs-attention queue', async () => { + vi.mocked(AIAPI.getUnifiedFindings).mockResolvedValueOnce({ + findings: [ + { + id: 'infra-warning', + source: 'ai-patrol', + severity: 'warning', + category: 'infrastructure', + resource_id: 'instance:node:101', + resource_name: 'db-01', + resource_type: 'host', + title: 'Disk nearly full', + description: 'Storage usage is high.', + detected_at: '2026-03-01T00:00:00Z', + status: 'active', + investigation_outcome: 'fix_verification_unknown', + }, + { + id: 'runtime-warning', + source: 'ai-patrol', + severity: 'warning', + category: 'service', + resource_id: 'ai-service', + resource_name: 'Pulse Patrol Service', + resource_type: 'service', + title: 'Pulse Patrol: Insufficient API credits', + description: 'Provider credits are exhausted.', + detected_at: '2026-03-01T00:01:00Z', + last_seen_at: '2026-03-01T00:05:00Z', + status: 'active', + investigation_outcome: 'fix_failed', + }, + ], + count: 2, + }); + + vi.mocked(AIAPI.getPendingApprovals).mockResolvedValueOnce([]); + + await aiIntelligenceStore.loadFindings(); + await aiIntelligenceStore.loadPendingApprovals(); + + expect(aiIntelligenceStore.findingsNeedingAttention.map((finding) => finding.id)).toEqual([ + 'runtime-warning', + 'infra-warning', + ]); + }); }); diff --git a/frontend-modern/src/stores/aiIntelligence.ts b/frontend-modern/src/stores/aiIntelligence.ts index e164b9105..99d7fab5e 100644 --- a/frontend-modern/src/stores/aiIntelligence.ts +++ b/frontend-modern/src/stores/aiIntelligence.ts @@ -22,6 +22,7 @@ import { doesFindingNeedAttention, hasPendingInvestigationFixApproval, isPatrolInvestigationFixApproval, + sortFindingsForAttentionQueue, } from '@/utils/aiFindingPresentation'; import { getApprovalExpiryTime, isLivePendingApproval } from '@/utils/approvalState'; import { logger } from '@/utils/logger'; @@ -453,7 +454,9 @@ export const aiIntelligenceStore = { get findingsNeedingAttention() { const approvals = getLivePendingApprovals(); - return unifiedFindings().filter((finding) => doesFindingNeedAttention(finding, approvals)); + return sortFindingsForAttentionQueue( + unifiedFindings().filter((finding) => doesFindingNeedAttention(finding, approvals)), + ); }, get needsAttentionCount() {