diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 1e8f63279..bbef547c0 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -241,6 +241,10 @@ instead of surfacing a peer `History` tab, so resource investigation stays on one coherent runtime surface: the overview card carries the compact recent activity summary, while the embedded change-history section owns filters and the event log without duplicating a second timeline-summary card. +That same overview now keeps AI intelligence and data-governance details +inside a collapsed `Investigation context` disclosure, so runtime status and +identity stay primary while secondary AI and policy signals remain available +without competing with the first-screen monitoring story. The same facet bundle now also returns grouped recent-change counts by canonical change kind, so the detail drawer can surface the distribution of state transitions, restarts, config updates, and anomalies without diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx index 326d52c66..3a37b2f40 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx @@ -141,6 +141,7 @@ const DrawerContent: Component = (props) => { const [debugEnabled] = createLocalStorageBooleanSignal(STORAGE_KEYS.DEBUG_MODE, false); const [copied, setCopied] = createSignal(false); const [showReportModal, setShowReportModal] = createSignal(false); + const [showInvestigationContext, setShowInvestigationContext] = createSignal(false); const displayName = createMemo(() => getPreferredResourceDisplayName(props.resource)); const kubernetesClusterName = createMemo(() => @@ -307,6 +308,27 @@ const DrawerContent: Component = (props) => { const resourceDependencies = createMemo(() => resourceIntelligence()?.dependencies ?? []); const resourceDependents = createMemo(() => resourceIntelligence()?.dependents ?? []); const resourceCorrelations = createMemo(() => resourceIntelligence()?.correlations ?? []); + const hasInvestigationContext = createMemo( + () => Boolean(resourceIntelligence()) || hasGovernanceData(), + ); + const investigationContextSummary = createMemo(() => { + const intel = resourceIntelligence(); + const summary: string[] = []; + + if (intel) { + summary.push(`AI health ${intel.health.grade} · ${Math.round(intel.health.score)}/100`); + } + if (resourceCorrelations().length > 0) { + summary.push( + `${resourceCorrelations().length} correlation${resourceCorrelations().length === 1 ? '' : 's'}`, + ); + } + if (props.resource.policy?.routing.scope) { + summary.push(`Routing ${getResourceRoutingScopeLabel(props.resource.policy.routing.scope)}`); + } + + return summary.join(' · '); + }); const timelineFacetRequest = createMemo(() => { const id = resourceFacetId(); if (!id) return null; @@ -791,56 +813,6 @@ const DrawerContent: Component = (props) => {
- - {(intel) => ( -
-
- AI Intelligence -
-
-
- Health - - {intel().health.grade} · {Math.round(intel().health.score)}/100 - -
-
- Trend - - {intel().health.trend} - -
-
- Notes - {intel().note_count} -
-
- Recent Changes - - {intel().recent_changes?.length ?? 0} - -
- - -
-
- )} -
-
Runtime @@ -1003,52 +975,6 @@ const DrawerContent: Component = (props) => {
- -
-
- Data Governance -
-
- -
- Sensitivity - - {getResourceSensitivityLabel(props.resource.policy?.sensitivity)} - -
-
- Routing - - {getResourceRoutingScopeLabel(props.resource.policy?.routing.scope)} - -
-
- 0}> -
- Redactions -
- - {(label) => ( - - {label} - - )} - -
-
-
- -
- AI-Safe Summary -
- {governanceSummary()} -
-
-
-
-
-
-
@@ -1417,6 +1343,131 @@ const DrawerContent: Component = (props) => {
+ +
+
+
+
+ Investigation context +
+
+ Secondary AI and policy signals for deeper investigation. +
+ +
+ {investigationContextSummary()} +
+
+
+ + +
+ + +
+ + {(intel) => ( +
+
+ AI Intelligence +
+
+
+ Health + + {intel().health.grade} · {Math.round(intel().health.score)}/100 + +
+
+ Trend + + {intel().health.trend} + +
+
+ Notes + {intel().note_count} +
+ + +
+
+ )} +
+ + +
+
+ Data Governance +
+
+ +
+ Sensitivity + + {getResourceSensitivityLabel(props.resource.policy?.sensitivity)} + +
+
+ Routing + + {getResourceRoutingScopeLabel(props.resource.policy?.routing.scope)} + +
+
+ 0}> +
+ Redactions +
+ + {(label) => ( + + {label} + + )} + +
+
+
+ +
+ AI-Safe Summary +
+ {governanceSummary()} +
+
+
+
+
+
+
+
+
+
+ {(config) => (
diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx index 0cb06cef4..b87c2bf7c 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx @@ -194,8 +194,9 @@ describe('ResourceDetailDrawer change history section', () => { expect(screen.getAllByText('Pulse diff 2')).toHaveLength(1); expect(screen.getAllByText('Docker adapter 2')).toHaveLength(1); expect(screen.getAllByText('Proxmox adapter 1')).toHaveLength(1); - expect(screen.getByText('Storage 1 alias')).toBeInTheDocument(); - expect(screen.getByText('VM Child')).toBeInTheDocument(); + expect(screen.getByText('Investigation context')).toBeInTheDocument(); + expect(screen.queryByText('Storage 1 alias')).toBeNull(); + expect(screen.queryByText('VM Child')).toBeNull(); expect(screen.queryByText('Capabilities 1')).toBeNull(); expect(screen.queryByText('Relationships 1')).toBeNull(); }); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx index 18e4b9b7a..8c2618025 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.identity-runtime.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest'; -import { render, waitFor } from '@solidjs/testing-library'; +import { fireEvent, render, waitFor } from '@solidjs/testing-library'; import type { Resource } from '@/types/resource'; import { ResourceDetailDrawer } from '@/components/Infrastructure/ResourceDetailDrawer'; @@ -291,20 +291,24 @@ describe('ResourceDetailDrawer runtime and identity cards', () => { aiSafeSummary: 'restricted host summary safe for remote AI consumption', }); - const { getAllByText, getByText, queryByText } = render(() => ( + const { getAllByText, getByRole, getByText, queryByText } = render(() => ( )); + expect(getByText('Investigation context')).toBeInTheDocument(); + expect(queryByText('Data Governance')).toBeNull(); + expect(queryByText('AI-Safe Summary')).toBeNull(); + fireEvent.click(getByRole('button', { name: 'Show context' })); + expect(getByText('Data Governance')).toBeInTheDocument(); + expect(getByText('Redactions')).toBeInTheDocument(); + expect(getByText('AI-Safe Summary')).toBeInTheDocument(); expect(getAllByText('Restricted').length).toBeGreaterThan(0); expect(getAllByText('Local Only').length).toBeGreaterThan(0); + expect(getByText('Hostname')).toBeInTheDocument(); + expect(getByText('IP Address')).toBeInTheDocument(); expect( getAllByText('restricted host summary safe for remote AI consumption').length, ).toBeGreaterThan(0); - expect(getByText('Data Governance')).toBeInTheDocument(); - expect(getByText('Redactions')).toBeInTheDocument(); - expect(getByText('Hostname')).toBeInTheDocument(); - expect(getByText('IP Address')).toBeInTheDocument(); - expect(getByText('AI-Safe Summary')).toBeInTheDocument(); expect(queryByText('Sensitive Host')).toBeNull(); expect(queryByText('sensitive-host')).toBeNull(); }); @@ -323,23 +327,33 @@ describe('ResourceDetailDrawer runtime and identity cards', () => { }, }); - const { getAllByText, getByText } = render(() => ); + const { getAllByText, getByRole, getByText, queryByText } = render(() => ( + + )); + expect(getByText('Investigation context')).toBeInTheDocument(); + expect(queryByText('AI-Safe Summary')).toBeNull(); + fireEvent.click(getByRole('button', { name: 'Show context' })); expect(getByText('AI-Safe Summary')).toBeInTheDocument(); expect(getAllByText('redacted by policy').length).toBeGreaterThan(1); }); it('surfaces canonical AI intelligence for the resource overview', async () => { const resource = baseResource({}); - const { getByRole, getByText } = render(() => ); + const { getByRole, getByText, queryByText } = render(() => ( + + )); + await waitFor(() => { + expect(getByText('Investigation context')).toBeInTheDocument(); + }); + + expect(queryByText('AI Intelligence')).toBeNull(); + fireEvent.click(getByRole('button', { name: 'Show context' })); await waitFor(() => { expect(getByText('AI Intelligence')).toBeInTheDocument(); }); - - expect(getByText(/A · 92\/100/)).toBeInTheDocument(); - expect(getByText('Recent Changes')).toBeInTheDocument(); - expect(getByText('Recent Changes').parentElement).toHaveTextContent('1'); + expect(getByText('AI health A · 92/100 · 1 correlation')).toBeInTheDocument(); await waitFor(() => { expect(getByText('Correlation context')).toBeInTheDocument(); });