diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 77a911dfd..a3110d15e 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -80,6 +80,7 @@ Own canonical runtime payload shapes between backend and frontend. and the canonical policy-posture snapshot derived from unified resources, so sensitivity, routing, and redaction counts stay owned by the same AI summary contract instead of being reconstructed as a page-local governance rollup and the resource-intelligence payload carried by the drawer AI card, so the governed posture snapshot remains visible on the resource-detail surface without introducing a separate posture endpoint and the learned-correlation payload loaded into the shared AI intelligence store, so the Patrol intelligence page and the AI summary page consume the same governed correlation slice instead of each page fetching its own copy + and the shared dashboard-load bundle inside `frontend-modern/src/stores/aiIntelligence.ts`, so the page orchestration stays on the store-owned bundle instead of enumerating the AI fetches inline 8. Route frontend API-client parsed error propagation, API-error-status fallback handling, allowed-status handling, custom status-specific error handling, command-trigger success envelope handling, shared response parsing pipelines, missing-resource lookup handling, metadata CRUD routing, stream event consumption, response status, collection normalization, scalar payload coercion, and structured error normalization through canonical shared helpers under `frontend-modern/src/api/` 9. Add or change API token scope, assignment, and revocation presentation through `frontend-modern/src/components/Settings/APITokenManager.tsx` 10. Add or change infrastructure operations token generation, lookup, assignment, and reporting/install presentation through `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx` @@ -184,6 +185,9 @@ through the shared `frontend-modern/src/stores/aiIntelligence.ts` store for the Patrol intelligence page and the AI summary page, so the learned-correlation list is governed by the same API contract that backs the resource drawer's graph evidence instead of being fetched as page-local state. +That store now also owns the dashboard load bundle used by the Patrol page, +so the page refresh path stays aligned on one store-owned orchestration layer +instead of re-encoding the AI bundle inline. The unified action, lifecycle, and export audit reads now also clamp oversized `limit` requests to the governed maximum of `1000`, so the control-plane audit surface stays bounded even when callers ask for arbitrarily large history diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 564947d8f..c086ac041 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -104,6 +104,10 @@ from the canonical AI correlations endpoint through the shared `frontend-modern/src/stores/aiIntelligence.ts` store, so the global summary and the resource drawer both reflect the same learned edge evidence instead of each page fetching its own copy. +That same store now owns the Patrol dashboard load bundle as well, so the +page refresh path stays aligned on a single orchestrated AI bundle instead of +repeating the individual summary, findings, approval, and correlation fetches +inline. The Patrol page and resource drawer now also share the canonical `frontend-modern/src/utils/resourceChangePresentation.ts` formatter so recent-change kind and headline wording stays aligned wherever the canonical diff --git a/frontend-modern/src/pages/AIIntelligence.tsx b/frontend-modern/src/pages/AIIntelligence.tsx index 00d0e99c2..1bad16d93 100644 --- a/frontend-modern/src/pages/AIIntelligence.tsx +++ b/frontend-modern/src/pages/AIIntelligence.tsx @@ -761,14 +761,7 @@ export function AIIntelligence() { async function loadAllData() { setIsRefreshing(true); try { - await Promise.all([ - aiIntelligenceStore.loadIntelligenceSummary(), - aiIntelligenceStore.loadFindings(), - aiIntelligenceStore.loadCircuitBreakerStatus(), - aiIntelligenceStore.loadPendingApprovals(), - aiIntelligenceStore.loadCorrelations(), - refetchPatrolStatus(), - ]); + await Promise.all([aiIntelligenceStore.loadDashboardData(), refetchPatrolStatus()]); // Trigger refresh of patrol status bar setActivityRefreshTrigger((prev) => prev + 1); } finally { diff --git a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx index c36fa074c..af86c4f9a 100644 --- a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx +++ b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx @@ -144,8 +144,8 @@ vi.mock('@/stores/notifications', () => ({ }, })); -vi.mock('@/stores/aiIntelligence', () => ({ - aiIntelligenceStore: { +vi.mock('@/stores/aiIntelligence', () => { + const store = { findings: [], loadFindings: vi.fn().mockResolvedValue(undefined), loadIntelligenceSummary: vi.fn().mockResolvedValue(undefined), @@ -157,14 +157,27 @@ vi.mock('@/stores/aiIntelligence', () => ({ setCorrelationsState(response); return response; }), + loadDashboardData: vi.fn().mockImplementation(async () => { + await Promise.all([ + store.loadFindings(), + store.loadIntelligenceSummary(), + store.loadCircuitBreakerStatus(), + store.loadPendingApprovals(), + store.loadCorrelations(), + ]); + }), get intelligenceSummary() { return intelligenceState.summary; }, get correlations() { return correlationsState(); }, - }, -})); + }; + + return { + aiIntelligenceStore: store, + }; +}); vi.mock('@/components/AI/FindingsPanel', () => ({ FindingsPanel: (props: Record) => { diff --git a/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts b/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts index e7b6723dc..ba599dbf3 100644 --- a/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts +++ b/frontend-modern/src/stores/__tests__/aiIntelligence.test.ts @@ -166,6 +166,48 @@ describe('aiIntelligenceStore', () => { expect(aiIntelligenceStore.correlations?.correlations).toHaveLength(1); }); + it('loads the canonical AI dashboard bundle', async () => { + vi.mocked(AIAPI.getIntelligenceSummary).mockResolvedValueOnce({ + timestamp: '2026-03-01T00:00:00Z', + overall_health: { + score: 87, + grade: 'B', + trend: 'stable', + factors: [], + prediction: 'Stable', + }, + findings_count: { + critical: 0, + warning: 0, + watch: 0, + info: 0, + total: 0, + }, + predictions_count: 0, + recent_changes_count: 0, + recent_changes: [], + learning: { + resources_with_knowledge: 0, + total_notes: 0, + resources_with_baselines: 0, + patterns_detected: 0, + correlations_learned: 0, + incidents_tracked: 0, + }, + }); + vi.mocked(AIAPI.getCorrelations).mockResolvedValueOnce({ + correlations: [], + count: 0, + }); + + await aiIntelligenceStore.loadDashboardData(); + + expect(AIAPI.getIntelligenceSummary).toHaveBeenCalledTimes(1); + expect(AIAPI.getCorrelations).toHaveBeenCalledTimes(1); + expect(aiIntelligenceStore.intelligenceSummary?.findings_count.total).toBe(0); + expect(aiIntelligenceStore.correlations?.count).toBe(0); + }); + it('treats queued fixes without a live approval as findings needing attention', async () => { vi.mocked(AIAPI.getUnifiedFindings).mockResolvedValueOnce({ findings: [ diff --git a/frontend-modern/src/stores/aiIntelligence.ts b/frontend-modern/src/stores/aiIntelligence.ts index b06a255fd..961f4098d 100644 --- a/frontend-modern/src/stores/aiIntelligence.ts +++ b/frontend-modern/src/stores/aiIntelligence.ts @@ -522,6 +522,16 @@ export const aiIntelligenceStore = { } }, + async loadDashboardData() { + await Promise.all([ + this.loadIntelligenceSummary(), + this.loadFindings(), + this.loadCircuitBreakerStatus(), + this.loadPendingApprovals(), + this.loadCorrelations(), + ]); + }, + async loadIntelligenceSummary() { try { const summary = await AIAPI.getIntelligenceSummary(); @@ -546,12 +556,8 @@ export const aiIntelligenceStore = { // Initialize - load all data async initialize() { await Promise.all([ - this.loadIntelligenceSummary(), - this.loadFindings(), + this.loadDashboardData(), this.loadRemediationPlans(), - this.loadCircuitBreakerStatus(), - this.loadPendingApprovals(), - this.loadCorrelations(), ]); },