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.
This commit is contained in:
rcourtman
2026-03-25 21:41:59 +00:00
parent b0e76b00ae
commit dee01cb85f
4 changed files with 99 additions and 20 deletions
@@ -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<typeof useDashboardActions>;
createRoot((d) => {
dispose = d;
const [alertsList] = createSignal<Alert[]>([]);
actions = useDashboardActions(alertsList);
});
await Promise.resolve();
await Promise.resolve();
expect(actions.findingsNeedingAttention().map((finding) => finding.id)).toEqual([
'runtime-warning',
'infra-warning',
]);
dispose();
});
});
@@ -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<ApprovalRequest[]>;
@@ -55,7 +54,7 @@ export function useDashboardActions(alertsList: Accessor<Alert[]>): DashboardAct
const findingsNeedingAttention = createMemo(() => {
if (!hasPatrol()) return [];
return sortFindingsForAttentionQueue(aiIntelligenceStore.findingsNeedingAttention);
return aiIntelligenceStore.findingsNeedingAttention;
});
const totalActionCount = createMemo(
@@ -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',
]);
});
});
+4 -1
View File
@@ -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() {