From 2c0217ecfcc2015657af2926bc31f953eb7c0a85 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 19 Mar 2026 10:58:59 +0000 Subject: [PATCH] Refocus v6 resource history on investigation timeline --- docs/release-control/control_plane.json | 10 +- .../subsystems/performance-and-scalability.md | 10 + .../internal/subsystems/unified-resources.md | 12 +- .../Infrastructure/ResourceDetailDrawer.tsx | 355 ++++-------------- .../Infrastructure/ResourceFacetSummary.tsx | 26 +- .../Infrastructure/UnifiedResourceTable.tsx | 24 +- .../ResourceDetailDrawer.history.test.tsx | 40 +- ...esourceTable.performance.contract.test.tsx | 40 +- ...edResourceTable.policy-governance.test.tsx | 6 +- ...ifiedResourceTable.workloads-link.test.tsx | 8 +- .../control_plane_audit_test.py | 24 +- 11 files changed, 190 insertions(+), 365 deletions(-) diff --git a/docs/release-control/control_plane.json b/docs/release-control/control_plane.json index e071a2869f..038c322279 100644 --- a/docs/release-control/control_plane.json +++ b/docs/release-control/control_plane.json @@ -6,7 +6,7 @@ "control_plane_doc": "docs/release-control/internal/CONTROL_PLANE.md", "control_plane_schema": "docs/release-control/control_plane.schema.json", "active_profile_id": "v6", - "active_target_id": "v6-product-lane-expansion", + "active_target_id": "v6-rc-stabilization", "profiles": [ { "id": "v6", @@ -38,8 +38,8 @@ "id": "v6-rc-stabilization", "profile_id": "v6", "kind": "stabilization", - "status": "superseded", - "summary": "Keep Pulse v6 in RC stabilization until remaining product/runtime concerns are resolved or normalized into governed proofs before GA promotion is resumed, while broader post-RC polish stays out of the release floor by default.", + "status": "active", + "summary": "Keep Pulse v6 centered on the proven monitoring-and-alerting RC floor, using timeline/change intelligence only where it materially improves investigation or safe AI flows while broader control-plane expansion stays out of the default target until it is proven.", "completion_rule": "manual", "proof_scope": "none" }, @@ -55,8 +55,8 @@ "id": "v6-product-lane-expansion", "profile_id": "v6", "kind": "feature", - "status": "active", - "summary": "Default v6 engineering work to the bridge-release foundation by promoting and landing the known resource-graph, policy-governance, action-governance, and fleet-governance gaps ahead of further local RC cleanup.", + "status": "planned", + "summary": "Promote the known resource-graph, policy-governance, action-governance, and fleet-governance gaps only after the monitoring-first RC target is satisfied and the broader surfaced product case is proven.", "completion_rule": "manual", "proof_scope": "none" } diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 2a26fc6f75..3ebfc63aa1 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -96,12 +96,22 @@ forking separate table-only presentation logic. That component now also consumes the shared `frontend-modern/src/utils/resourceChangePresentation.ts` label helper for canonical change kinds, source types, and adapter provenance so the chip wording stays consistent without adding extra hot-path branching. +The default table hot path now scopes those summary chips to timeline and +change-provenance badges only. Generic capability and relationship counts are +kept out of the main row budget until the underlying data is proven populated, +which preserves the fleet-table scan path and avoids spending hot-path visual +budget on model nouns that do not yet clear the product bar. Row summaries now also prefer canonical `facetCounts` on each resource when they are available, so the hot path can stay within the same budget while still reading totals from the shared resource contract. The drawer history surface reuses the same governed resource route helpers for relationship and related-resource links, so cross-resource navigation stays within the existing infrastructure surface rather than branching into custom detail-only routing. +The detail drawer now follows the same default posture by collapsing its +history overview down to timeline counts and timeline-summary chips, so the +performance-sensitive shared presentation path stays aligned with the +investigation-first product contract instead of rendering low-signal generic +facet sections by default. Governance metadata such as sensitivity and routing scope may be visible in the table, but it must remain on the same bounded row-windowing and mounted-row budget proved by `UnifiedResourceTable.performance.contract.test.tsx` rather diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index db5d5b3b12..7f02ec71c7 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -212,14 +212,22 @@ The frontend now also consumes those facet reads through `frontend-modern/src/api/resources.ts` and the dedicated resource detail drawer, which keeps the presentation surface aligned with the governed API contract instead of rebuilding the graph and timeline inline. +The shared `ResourceFacetSummary` consumer can now explicitly suppress +capability and relationship badges, and the canonical table/detail surfaces +use that narrower mode by default. That keeps the proven monitoring UX +centered on factual timeline investigation while the richer facet payloads +remain available as backend and AI-facing foundations instead of being +presented as first-class product facts before they are fully populated. That drawer now also uses a shared frontend relationship-presentation helper for graph labels and provenance wording, so the UI stays aligned with the canonical relationship semantics instead of keeping drawer-local token humanization. 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, anomalies, relationships, and -capabilities without recomputing timeline history in the browser. +state transitions, restarts, config updates, and anomalies without +recomputing timeline history in the browser, while broader graph or +capability facets stay available behind the same contract for non-default +consumers that can prove they are populated and justified. That same facet bundle now also returns grouped recent-change provenance counts by source type, so the detail drawer can distinguish platform events, pulse diffs, heuristics, user actions, and agent actions without re-deriving diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx index 2469a941c7..180b70d12d 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx @@ -1,4 +1,12 @@ -import { Show, Suspense, createMemo, For, createSignal, createEffect, createResource } from 'solid-js'; +import { + Show, + Suspense, + createMemo, + For, + createSignal, + createEffect, + createResource, +} from 'solid-js'; import type { Component } from 'solid-js'; import type { Resource, @@ -52,12 +60,10 @@ import { getResourceRoutingScopeLabel, getResourceSensitivityLabel, } from '@/utils/resourcePolicyPresentation'; -import { describeResourceRelationship } from '@/utils/resourceRelationshipPresentation'; import { ResourceGraphSummary } from './ResourceGraphSummary'; import { ResourceChangeSummary } from './ResourceChangeSummary'; import { ResourceFacetSummary } from './ResourceFacetSummary'; import { ResourcePolicySummary } from './ResourcePolicySummary'; -import { getResourceApprovalLevelLabel } from '@/utils/approvalPresentation'; import { RESOURCE_CHANGE_KIND_ORDER, RESOURCE_CHANGE_SOURCE_ADAPTER_ORDER, @@ -67,7 +73,7 @@ import { getResourceChangeSourceTypePresentation, } from '@/utils/resourceChangePresentation'; import { formatConfidenceLabel } from '@/utils/confidencePresentation'; -import { formatIdentifierLabel, humanizeToken } from '@/utils/textPresentation'; +import { formatIdentifierLabel } from '@/utils/textPresentation'; import type { ResourceIntelligence } from '@/types/aiIntelligence'; import { buildInfrastructureResourceHref } from '@/routing/resourceLinks'; @@ -109,14 +115,16 @@ const timelineSourceTypeOptions: Array<{ label: string; value: ResourceChangeSou })), ]; -const timelineSourceAdapterOptions: Array<{ label: string; value: ResourceChangeSourceAdapter | '' }> = - [ - { label: 'All adapters', value: '' }, - ...RESOURCE_CHANGE_SOURCE_ADAPTER_ORDER.map((sourceAdapter) => ({ - label: getResourceChangeSourceAdapterPresentation(sourceAdapter).label, - value: sourceAdapter, - })), - ]; +const timelineSourceAdapterOptions: Array<{ + label: string; + value: ResourceChangeSourceAdapter | ''; +}> = [ + { label: 'All adapters', value: '' }, + ...RESOURCE_CHANGE_SOURCE_ADAPTER_ORDER.map((sourceAdapter) => ({ + label: getResourceChangeSourceAdapterPresentation(sourceAdapter).label, + value: sourceAdapter, + })), +]; const DrawerContent: Component = (props) => { type DrawerTab = @@ -149,7 +157,9 @@ const DrawerContent: Component = (props) => { }); const hasUnifiedSources = createMemo(() => unifiedSourceBadges().length > 0); const policyBadges = createMemo(() => getResourcePolicyBadges(props.resource.policy)); - const policyRedactions = createMemo(() => getResourcePolicyRedactionLabels(props.resource.policy)); + const policyRedactions = createMemo(() => + getResourcePolicyRedactionLabels(props.resource.policy), + ); const policyRoutingDecisions = createMemo(() => getResourcePolicyRoutingDecisionSummaries(props.resource.policy), ); @@ -260,10 +270,12 @@ const DrawerContent: Component = (props) => { const [k8sDeploymentsPrefillNamespace, setK8sDeploymentsPrefillNamespace] = createSignal(''); const resourceFacetId = createMemo(() => props.resource.id.trim()); const [timelineKindFilter, setTimelineKindFilter] = createSignal(''); - const [timelineSourceTypeFilter, setTimelineSourceTypeFilter] = - createSignal(''); - const [timelineSourceAdapterFilter, setTimelineSourceAdapterFilter] = - createSignal(''); + const [timelineSourceTypeFilter, setTimelineSourceTypeFilter] = createSignal< + ResourceChangeSourceType | '' + >(''); + const [timelineSourceAdapterFilter, setTimelineSourceAdapterFilter] = createSignal< + ResourceChangeSourceAdapter | '' + >(''); const resourceFacetRequest = createMemo(() => { const id = resourceFacetId(); return id ? { id } : null; @@ -381,12 +393,6 @@ const DrawerContent: Component = (props) => { const nonZero = all.filter((entry) => entry.value > 0); return nonZero.length > 0 ? nonZero : all; }); - const resourceCapabilities = createMemo( - () => resourceFacets()?.capabilities ?? props.resource.capabilities ?? [], - ); - const resourceRelationships = createMemo( - () => resourceFacets()?.relationships ?? props.resource.relationships ?? [], - ); const resourceTimeline = createMemo( () => resourceFacets()?.recentChanges ?? props.resource.recentChanges ?? [], ); @@ -394,44 +400,36 @@ const DrawerContent: Component = (props) => { () => resourceFacets()?.counts ?? props.resource.facetCounts ?? null, ); const historyFacetBundle = createMemo(() => - timelineFacetRequest() ? timelineFacets() ?? resourceFacets() : resourceFacets(), + timelineFacetRequest() ? (timelineFacets() ?? resourceFacets()) : resourceFacets(), ); - const historyCapabilities = createMemo( - () => historyFacetBundle()?.capabilities ?? resourceCapabilities(), + const historyTimeline = createMemo( + () => historyFacetBundle()?.recentChanges ?? resourceTimeline(), ); - const historyRelationships = createMemo( - () => historyFacetBundle()?.relationships ?? resourceRelationships(), - ); - const historyTimeline = createMemo(() => historyFacetBundle()?.recentChanges ?? resourceTimeline()); const historyFacetCounts = createMemo( () => historyFacetBundle()?.counts ?? resourceFacetCounts(), ); - const hasTimelineFilters = createMemo( - () => - Boolean( - timelineKindFilter() || timelineSourceTypeFilter() || timelineSourceAdapterFilter(), - ), - ); - const resourceCapabilityCount = createMemo( - () => resourceFacetCounts()?.capabilities ?? resourceCapabilities().length, - ); - const resourceRelationshipCount = createMemo( - () => resourceFacetCounts()?.relationships ?? resourceRelationships().length, + const hasTimelineFilters = createMemo(() => + Boolean(timelineKindFilter() || timelineSourceTypeFilter() || timelineSourceAdapterFilter()), ); const resourceTimelineCount = createMemo( () => resourceFacetCounts()?.recentChanges ?? resourceTimeline().length, ); + const historyTimelineCount = createMemo( + () => historyFacetCounts()?.recentChanges ?? historyTimeline().length, + ); const sortedResourceTimeline = createMemo(() => [...historyTimeline()].sort((left, right) => { const leftTime = Date.parse(left.observedAt || ''); const rightTime = Date.parse(right.observedAt || ''); - return (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0); + return ( + (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0) + ); }), ); const facetBundleError = createMemo(() => { const error = timelineFacetRequest() ? timelineFacets.error : resourceFacets.error; if (!error) return ''; - return (error as Error)?.message || 'Failed to load resource facets'; + return (error as Error)?.message || 'Failed to load resource history'; }); const refetchHistoryFacets = () => { if (timelineFacetRequest()) { @@ -784,7 +782,7 @@ const DrawerContent: Component = (props) => { )} - +
@@ -874,20 +872,14 @@ const DrawerContent: Component = (props) => {
- 0 || - resourceRelationships().length > 0 || - resourceTimeline().length > 0 - } - > + 0}>
- Facets + Timeline
@@ -1113,7 +1105,9 @@ const DrawerContent: Component = (props) => { - {formatIdentifierLabel(dockerHostCommand()?.status, { fallback: 'unknown' })} + {formatIdentifierLabel(dockerHostCommand()?.status, { + fallback: 'unknown', + })} @@ -1455,16 +1449,14 @@ const DrawerContent: Component = (props) => {
{timelineFacetRequest() ? timelineFacets.loading - ? 'Refreshing filtered facet data...' - : 'Filtered facet data loaded' + ? 'Refreshing filtered history...' + : 'Filtered history loaded' : resourceFacets.loading - ? 'Refreshing facet data...' - : 'Facet data loaded'} + ? 'Refreshing history...' + : 'History loaded'}
-
- History filters active -
+
History filters active
@@ -1476,7 +1468,9 @@ const DrawerContent: Component = (props) => { class="w-full rounded border border-border bg-base px-2 py-1 text-[11px] text-base-content" value={timelineKindFilter()} onChange={(event) => - setTimelineKindFilter((event.currentTarget.value || '') as ResourceChangeKind | '') + setTimelineKindFilter( + (event.currentTarget.value || '') as ResourceChangeKind | '', + ) } > @@ -1486,9 +1480,9 @@ const DrawerContent: Component = (props) => { + +