From 3d4c43162a2e05e86b486fc7ff24a677bf73036d Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 27 Aug 2026 18:20:03 +0100 Subject: [PATCH] feat(alerts): add recurring scoped maintenance --- .../v6/internal/subsystems/agent-lifecycle.md | 5 +- .../v6/internal/subsystems/ai-runtime.md | 7 +- .../v6/internal/subsystems/alerts.md | 8 +- .../v6/internal/subsystems/api-contracts.md | 23 +- .../subsystems/frontend-primitives.md | 192 ++-- .../v6/internal/subsystems/monitoring.md | 19 +- .../subsystems/performance-and-scalability.md | 8 + .../v6/internal/subsystems/registry.json | 15 + .../internal/subsystems/security-privacy.md | 6 + .../internal/subsystems/storage-recovery.md | 4 +- .../internal/subsystems/unified-resources.md | 875 +++++++++--------- frontend-modern/browser-verification.json | 33 +- .../__tests__/resourceOperatorState.test.ts | 10 +- .../src/api/resourceOperatorState.ts | 50 +- .../ResourceOperatorStateSection.tsx | 288 ++++-- .../ResourceOperatorStateSection.test.ts | 25 +- .../alerts/ResourceMonitoringPolicyAction.tsx | 2 + .../pages/__tests__/Alerts.helpers.test.ts | 10 + internal/agentcapabilities/manifest.go | 29 +- internal/agentcapabilities/manifest_test.go | 15 +- internal/alerts/intent_policy.go | 3 + internal/alerts/intent_policy_test.go | 19 +- internal/alerts/resource_monitoring_policy.go | 30 + internal/api/agent_resource_context.go | 26 +- internal/api/resources_operator_state.go | 63 +- internal/api/resources_operator_state_test.go | 19 + internal/api/router.go | 5 + internal/maintenancesentinel/sentinel.go | 54 +- internal/maintenancesentinel/sentinel_test.go | 35 + internal/monitoring/monitor_alert_intent.go | 60 +- .../monitoring/monitor_alert_intent_test.go | 58 ++ .../unifiedresources/loop_reports_store.go | 59 +- internal/unifiedresources/monitor_adapter.go | 37 + .../monitor_adapter_read_state_test.go | 25 + .../resource_operator_state.go | 306 +++++- .../resource_operator_state_test.go | 59 ++ internal/unifiedresources/store.go | 55 +- internal/unifiedresources/store_test.go | 18 + .../canonical_completion_guard_test.py | 1 + 39 files changed, 1762 insertions(+), 794 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 7d8ce4895..c90f97b0c 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -3294,7 +3294,8 @@ restarts because the underlying state lives in the durable The `/api/resources/{id}/operator-state` GET / PUT / DELETE handlers in `internal/api/resources_operator_state.go` are the canonical operator surface for setting per-resource intent (intentionally offline, never -auto-remediate, maintenance window, criticality hint). The route lives +auto-remediate, mutually exclusive one-shot or timezone-aware recurring +maintenance with explicit resource/descendant scope, criticality hint). The route lives on the same monitoring router (`router_routes_monitoring.go`) as the rest of `/api/resources/{id}/...`; method-keyed scope dispatch means GET runs under `monitoring:read` while PUT and DELETE require @@ -3302,7 +3303,7 @@ runs under `monitoring:read` while PUT and DELETE require against the resource. The agent runtime must surface the same operator-set state across restarts — persistence is in the `resource_operator_state` SQLite table managed by the unified-resources -store from slice 29 — so a maintenance window or never-auto-remediate +store from slice 29 — so either maintenance schedule form or never-auto-remediate flag set before a process restart is honored after the agent reloads. Patrol-finding to unified-finding mirroring in `internal/api/router.go` diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index f65a3f1f8..695cbaf23 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -5164,7 +5164,12 @@ new-finding path auto-dismisses with reason `expected_behavior`, attributes the suppression on the lifecycle timeline (`operator_state_cause: maintenance_window`, with `maintenance_end_at` metadata), and persists the finding for audit -history. The action broker consults the same `resource_operator_state` table +history. The active boundary may come from a one-shot or recurring schedule; +both are evaluated by the canonical operator-state model. Descendant-scope +inheritance is alert-intent behavior and does not weaken exact-resource +action-remediation locks or invent inherited action authority. + +The action broker consults the same `resource_operator_state` table on every dispatch — both the agent-command path (`executeCommandWithAudit`) and the native provider path (`executeNativeActionWithAudit`) in diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index fa6f28a78..8d843dd39 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -2028,7 +2028,13 @@ durable pending state and its transient monotonic baseline before a later outage can start. Operator maintenance and intentionally-offline state are read only through the -canonical unified-resource identity. Backup-aware offline deferral consumes +canonical unified-resource identity. One-shot and recurring maintenance use +the same effective occurrence boundary. Exact-resource maintenance always +applies; ancestor maintenance applies only when its persisted scope is +`resource_and_descendants`. Overlapping exact and inherited windows remain +suppressed until the latest active end, and every operator-state mutation +reconciles the active-alert set immediately so a host window cannot leave child +alerts visible until another detector write. Backup-aware offline deferral consumes fresh, matching, active task evidence, applies the configured post-backup grace, and always terminates at its hard cap. Missing, stale, future-skewed, finished, or mismatched backup evidence cannot suppress an outage. This policy diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 34b89981f..9172f3c70 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -5193,9 +5193,12 @@ mirrors the canonical Go shape from `getResourceOperatorState`, `setResourceOperatorState`, and `clearResourceOperatorState` against the `/api/resources/{id}/operator-state` endpoint. The GET path -normalizes the server's `404 operator_state_not_set` response into -`null` so callers see "no state recorded" as a clean default rather -than a thrown error; non-404 errors propagate. The PUT path +uses `view=lookup` so an unset record is represented by a successful +`{ "configured": false }` envelope rather than a routine failed browser +request. Persisted records return `{ "configured": true, "state": {...} }`. +The client continues to normalize an older server's `404 +operator_state_not_set` response into `null` during rolling updates; +non-404 errors propagate. The PUT path percent-encodes the canonical resource id segment so colon-bearing ids round-trip safely through URL routing. Operator-state reads must keep `autoRemediationPolicy.capabilityNames` @@ -5204,6 +5207,13 @@ capability list before JSON serialization, while the TS compatibility type and rendering path continue to tolerate `null` from pre-fix or cached payloads. Saving only `intentionallyOffline`, `neverAutoRemediate`, priority, or note must therefore never produce a drawer-breaking operator-state response. +The same read/write shape carries either the legacy one-shot start/end pair or +`maintenanceRecurrence` (`timezone`, canonical weekdays, start/end minutes), +never both, plus `maintenanceScope` (`resource` by default or +`resource_and_descendants`). The resource editor exposes both schedule forms, +IANA timezone, overnight recurrence, reason, and explicit descendant scope; +unrelated override saves and alert-card monitoring actions round-trip all +maintenance fields instead of clobbering them. The router wires the operator-state adapter into the findings runtime at startup: `internal/api/router.go` calls @@ -5224,7 +5234,8 @@ and without growing per-finding lookups as new signals land. `/api/resources/{id}/operator-state` is the canonical surface for operator-set per-resource intent (intentionally offline, never -auto-remediate, maintenance window, criticality hint). GET requires +auto-remediate, one-shot or recurring maintenance window with explicit scope, +criticality hint). GET requires `monitoring:read` and returns `404` with `{ "error": "operator_state_not_set" }` when no entry exists; PUT and DELETE require `monitoring:write` because they modulate Patrol's behavior on @@ -5242,6 +5253,10 @@ message. DELETE is idempotent (`204` whether or not an entry was present). The handler dispatches off `r.Method` rather than mounting three sibling routes so the URL surface stays a single resource path matching the rest of `/api/resources/{id}/...`. +Interactive clients may request `GET .../operator-state?view=lookup`; this +preserves the configured-versus-unset distinction in a 200 response envelope +and prevents a normal empty state from appearing as a failed browser request. +The default GET behavior and stable agent error code remain unchanged. The action governance loop at `/api/actions/plan`, `/api/actions/{id}/decision`, and `/api/actions/{id}/execute` is diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 9924fde4a..010952714 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -182,7 +182,6 @@ metrics target. Zero or multiple matches must leave the PBS resource unchanged rather than guessing; this presentation correlation must not mutate either canonical input or create a second mobile disclosure interaction. - Presentation helpers that mirror a server-side classification must name the predicate they mirror and expose it as a single exported function rather than inlining the boundary at each call site. `isPhysicalDiskWearoutReported` mirrors @@ -292,67 +291,67 @@ overflow. 71. `frontend-modern/src/utils/systemLogsPresentation.ts` 72. `frontend-modern/src/components/Settings/__tests__/SystemLogsPanel.test.tsx` 73. `frontend-modern/src/components/Settings/ResourcePicker.tsx` -75. `frontend-modern/src/utils/reportableResourceTypes.ts` -76. `frontend-modern/src/utils/reportingResourceTypes.ts` -77. `frontend-modern/src/utils/workloadEmptyStatePresentation.ts` -78. `frontend-modern/src/utils/workloadGuestPresentation.ts` -79. `frontend-modern/src/utils/emptyStatePresentation.ts` -80. `frontend-modern/src/utils/semanticTonePresentation.ts` -81. `frontend-modern/src/components/Toast/Toast.tsx` -82. `frontend-modern/src/utils/toast.ts` -83. `frontend-modern/src/utils/semanticTonePresentation.ts` -84. `frontend-modern/src/utils/emptyStatePresentation.ts` -85. `frontend-modern/src/utils/typeColumnPresentation.ts` -86. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx` -87. `frontend-modern/src/components/Settings/networkSettingsModel.ts` -88. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts` -89. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` -90. `frontend-modern/src/components/Settings/AvailabilitySettingsPanel.tsx` -91. `frontend-modern/src/components/Settings/availabilitySettingsModel.ts` -92. `frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx` -93. `frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts` -94. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` -95. `frontend-modern/src/components/Settings/settingsNavCatalog.ts` -96. `frontend-modern/src/components/Settings/settingsNavVisibility.ts` -97. `frontend-modern/src/components/Settings/settingsRouting.ts` -98. `frontend-modern/src/components/Settings/settingsTabSaveBehavior.ts` -99. `frontend-modern/src/components/Settings/settingsTypes.ts` -100. `frontend-modern/src/components/Settings/useSettingsNavigation.ts` -101. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx` -102. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx` -103. `frontend-modern/src/components/Settings/DockerRuntimeSettingsCard.tsx` -104. `frontend-modern/src/components/shared/EnvironmentLockBadge.tsx` -105. `frontend-modern/src/utils/environmentLockPresentation.ts` -106. `frontend-modern/src/utils/docsLinks.ts` -107. `tests/integration/tests/20-local-doc-links.spec.ts` -108. `frontend-modern/src/index.css` -109. `frontend-modern/src/components/shared/summaryInteractionA11y.ts` -110. `frontend-modern/src/components/shared/SummaryRowActionButton.tsx` -111. `frontend-modern/src/hooks/createNonSuspendingQuery.ts` +74. `frontend-modern/src/utils/reportableResourceTypes.ts` +75. `frontend-modern/src/utils/reportingResourceTypes.ts` +76. `frontend-modern/src/utils/workloadEmptyStatePresentation.ts` +77. `frontend-modern/src/utils/workloadGuestPresentation.ts` +78. `frontend-modern/src/utils/emptyStatePresentation.ts` +79. `frontend-modern/src/utils/semanticTonePresentation.ts` +80. `frontend-modern/src/components/Toast/Toast.tsx` +81. `frontend-modern/src/utils/toast.ts` +82. `frontend-modern/src/utils/semanticTonePresentation.ts` +83. `frontend-modern/src/utils/emptyStatePresentation.ts` +84. `frontend-modern/src/utils/typeColumnPresentation.ts` +85. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx` +86. `frontend-modern/src/components/Settings/networkSettingsModel.ts` +87. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts` +88. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts` +89. `frontend-modern/src/components/Settings/AvailabilitySettingsPanel.tsx` +90. `frontend-modern/src/components/Settings/availabilitySettingsModel.ts` +91. `frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx` +92. `frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts` +93. `frontend-modern/src/components/Settings/settingsNavigationModel.ts` +94. `frontend-modern/src/components/Settings/settingsNavCatalog.ts` +95. `frontend-modern/src/components/Settings/settingsNavVisibility.ts` +96. `frontend-modern/src/components/Settings/settingsRouting.ts` +97. `frontend-modern/src/components/Settings/settingsTabSaveBehavior.ts` +98. `frontend-modern/src/components/Settings/settingsTypes.ts` +99. `frontend-modern/src/components/Settings/useSettingsNavigation.ts` +100. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx` +101. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx` +102. `frontend-modern/src/components/Settings/DockerRuntimeSettingsCard.tsx` +103. `frontend-modern/src/components/shared/EnvironmentLockBadge.tsx` +104. `frontend-modern/src/utils/environmentLockPresentation.ts` +105. `frontend-modern/src/utils/docsLinks.ts` +106. `tests/integration/tests/20-local-doc-links.spec.ts` +107. `frontend-modern/src/index.css` +108. `frontend-modern/src/components/shared/summaryInteractionA11y.ts` +109. `frontend-modern/src/components/shared/SummaryRowActionButton.tsx` +110. `frontend-modern/src/hooks/createNonSuspendingQuery.ts` 111a. `frontend-modern/src/utils/storageSummaryCache.ts` -112. `frontend-modern/src/components/shared/TableCardHeader.tsx` -113. `frontend-modern/src/components/shared/UpgradeLink.tsx` -114. `frontend-modern/src/components/shared/useUpgradeNavigation.ts` -115. `frontend-modern/src/utils/upgradeNavigation.ts` -116. `frontend-modern/src/components/DemoBanner.tsx` +111. `frontend-modern/src/components/shared/TableCardHeader.tsx` +112. `frontend-modern/src/components/shared/UpgradeLink.tsx` +113. `frontend-modern/src/components/shared/useUpgradeNavigation.ts` +114. `frontend-modern/src/utils/upgradeNavigation.ts` +115. `frontend-modern/src/components/DemoBanner.tsx` 116a. `frontend-modern/src/components/CommercialMigrationBanner.tsx` 116b. `frontend-modern/src/components/GitHubStarBanner.tsx` -117. `frontend-modern/src/components/Login.tsx` -118. `frontend-modern/src/stores/sessionCapabilities.ts` -119. `frontend-modern/src/stores/sessionPresentationPolicy.ts` -120. `frontend-modern/src/stores/licenseCommercial.ts` -121. `frontend-modern/src/useAppRuntimeState.ts` -122. `frontend-modern/src/routing/routePreload.ts` -123. `frontend-modern/src/stores/aiChat.ts` -124. `frontend-modern/scripts/header-audit.mjs` -125. `frontend-modern/src/components/Settings/DataHandlingPanel.tsx` -126. `frontend-modern/src/components/Settings/dataHandlingPanelModel.ts` -127. `frontend-modern/scripts/canonical-platform-audit.mjs` -128. `frontend-modern/scripts/settings-diagnostics-boundary-audit.mjs` -129. `frontend-modern/scripts/shared-template-audit.mjs` -130. `frontend-modern/scripts/shared-template-registry.json` -131. `frontend-modern/src/features/platformPage/sharedPlatformPage.tsx` - 131a. `frontend-modern/src/features/platformPage/platformSearchSuggestions.ts` +116. `frontend-modern/src/components/Login.tsx` +117. `frontend-modern/src/stores/sessionCapabilities.ts` +118. `frontend-modern/src/stores/sessionPresentationPolicy.ts` +119. `frontend-modern/src/stores/licenseCommercial.ts` +120. `frontend-modern/src/useAppRuntimeState.ts` +121. `frontend-modern/src/routing/routePreload.ts` +122. `frontend-modern/src/stores/aiChat.ts` +123. `frontend-modern/scripts/header-audit.mjs` +124. `frontend-modern/src/components/Settings/DataHandlingPanel.tsx` +125. `frontend-modern/src/components/Settings/dataHandlingPanelModel.ts` +126. `frontend-modern/scripts/canonical-platform-audit.mjs` +127. `frontend-modern/scripts/settings-diagnostics-boundary-audit.mjs` +128. `frontend-modern/scripts/shared-template-audit.mjs` +129. `frontend-modern/scripts/shared-template-registry.json` +130. `frontend-modern/src/features/platformPage/sharedPlatformPage.tsx` + 131a. `frontend-modern/src/features/platformPage/platformSearchSuggestions.ts` 131b. `frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx` 131c. `frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.tsx` 131d. `frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx` @@ -361,23 +360,23 @@ overflow. 131g. `frontend-modern/src/components/shared/FormSelect.tsx` 131h. `frontend-modern/src/components/shared/FormTextarea.tsx` 131i. `frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx` -132. `frontend-modern/src/utils/platformSupportManifest.generated.ts` -133. `frontend-modern/src/utils/platformSupportManifest.ts` -134. `frontend-modern/src/utils/sourcePlatformOptions.ts` -135. `frontend-modern/src/utils/sourcePlatforms.ts` -136. `frontend-modern/src/utils/infrastructureOnboardingPresentation.ts` -137. `frontend-modern/src/components/shared/Button.tsx` -138. `frontend-modern/src/components/shared/buttonModel.ts` -139. `frontend-modern/src/components/shared/Button.test.tsx` +131. `frontend-modern/src/utils/platformSupportManifest.generated.ts` +132. `frontend-modern/src/utils/platformSupportManifest.ts` +133. `frontend-modern/src/utils/sourcePlatformOptions.ts` +134. `frontend-modern/src/utils/sourcePlatforms.ts` +135. `frontend-modern/src/utils/infrastructureOnboardingPresentation.ts` +136. `frontend-modern/src/components/shared/Button.tsx` +137. `frontend-modern/src/components/shared/buttonModel.ts` +138. `frontend-modern/src/components/shared/Button.test.tsx` 139a. `frontend-modern/src/components/shared/InlineNotice.tsx` 139b. `frontend-modern/src/components/shared/InlineNotice.test.tsx` 139c. `frontend-modern/src/components/shared/ExternalTextLink.tsx` 139d. `frontend-modern/src/components/shared/ExternalTextLink.test.tsx` -140. `frontend-modern/src/components/shared/CopyableCodeRow.tsx` -141. `frontend-modern/src/components/shared/DetailSectionTable.tsx` -142. `frontend-modern/src/components/shared/detailSectionModel.ts` -143. `frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts` -144. `frontend-modern/src/i18n/__tests__/i18n.test.ts` +139. `frontend-modern/src/components/shared/CopyableCodeRow.tsx` +140. `frontend-modern/src/components/shared/DetailSectionTable.tsx` +141. `frontend-modern/src/components/shared/detailSectionModel.ts` +142. `frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts` +143. `frontend-modern/src/i18n/__tests__/i18n.test.ts` ## Shared Boundaries @@ -720,8 +719,8 @@ scan-friendly without hiding any container from drilldown. 1. `frontend-modern/src/components/CommercialMigrationBanner.tsx` shared with `cloud-paid`: the global commercial migration notice is both a cloud-paid entitlement recovery surface and a shared app-shell notice primitive consumer. 2. `frontend-modern/src/components/Infrastructure/useTableWindowing.ts` shared with `performance-and-scalability`: the shared bounded table-window controller is both a canonical frontend rendering primitive and a fleet-scale scrolling hot-path boundary. -2. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` shared with `ai-runtime`, `api-contracts`: the External agents settings panel is the optional settings-shell projection of Pulse MCP onboarding, the AI runtime connected-agent onboarding surface, and a presentation consumer of the shared agent capabilities frontend client. -3. `frontend-modern/src/components/Settings/APIAccessPanel.tsx` shared with `security-privacy`: the API Access settings intro is both a security/privacy token-management trust surface and a canonical settings-shell presentation boundary. +3. `frontend-modern/src/components/Settings/AgentIntegrationsPanel.tsx` shared with `ai-runtime`, `api-contracts`: the External agents settings panel is the optional settings-shell projection of Pulse MCP onboarding, the AI runtime connected-agent onboarding surface, and a presentation consumer of the shared agent capabilities frontend client. +4. `frontend-modern/src/components/Settings/APIAccessPanel.tsx` shared with `security-privacy`: the API Access settings intro is both a security/privacy token-management trust surface and a canonical settings-shell presentation boundary. The panel may own shell placement and local action layout, but token-specific Docker / Podman copy must come from `frontend-modern/src/utils/apiTokenPresentation.ts` rather than page-local @@ -811,9 +810,9 @@ AGENT_SURFACE_ID_PULSE_MCP)` and `getAgentSurfaceToolPosturePresentation`, the Agent integrations panel are settings-shell chrome only: they may route to the API Access token creation section, but token preset semantics and required-scope derivation remain owned by the API/security boundary. -4. `frontend-modern/src/components/Settings/DataHandlingPanel.tsx` shared with `security-privacy`: the data-handling settings surface is both a security/privacy trust surface and a canonical settings-shell presentation boundary. -5. `frontend-modern/src/components/Settings/dataHandlingPanelModel.ts` shared with `security-privacy`: the data-handling settings model is both a security/privacy posture projection and a canonical settings-shell presentation boundary. -6. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` shared with `security-privacy`: the general settings privacy panel is both a security/privacy control surface and a canonical settings-shell presentation boundary. +5. `frontend-modern/src/components/Settings/DataHandlingPanel.tsx` shared with `security-privacy`: the data-handling settings surface is both a security/privacy trust surface and a canonical settings-shell presentation boundary. +6. `frontend-modern/src/components/Settings/dataHandlingPanelModel.ts` shared with `security-privacy`: the data-handling settings model is both a security/privacy posture projection and a canonical settings-shell presentation boundary. +7. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` shared with `security-privacy`: the general settings privacy panel is both a security/privacy control surface and a canonical settings-shell presentation boundary. The panel owns compact settings-shell framing for outbound usage telemetry, but its vocabulary must stay aligned with `security-privacy`: coarse deployment and lifecycle buckets, aggregate resource and outcome counts, coarse feature @@ -822,8 +821,8 @@ AGENT_SURFACE_ID_PULSE_MCP)` and `getAgentSurfaceToolPosturePresentation`, URLs, paths, locale, browser events, prompts, chat messages, command text, action output, token values, and personal information must stay explicitly excluded. -7. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` shared with `security-privacy`: the authentication settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. -8. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` shared with `security-privacy`: the security overview settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. +8. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` shared with `security-privacy`: the authentication settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. +9. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` shared with `security-privacy`: the security overview settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. These settings panels consume the privileged security-status projection, while the shared status type also represents intentionally sparse public and authenticated tiers. Privileged posture booleans therefore remain optional @@ -834,8 +833,8 @@ AGENT_SURFACE_ID_PULSE_MCP)` and `getAgentSurfaceToolPosturePresentation`, 10. `frontend-modern/src/features/platformPage/PlatformWindowedList.tsx` shared with `performance-and-scalability`: the shared bounded list renderer is both a canonical platform-page primitive and a fleet-scale mounted-DOM performance boundary. 11. `frontend-modern/src/features/platformPage/PlatformWindowedRows.tsx` shared with `performance-and-scalability`: the shared bounded table-row renderer is both a canonical platform-page primitive and a fleet-scale mounted-DOM performance boundary. 12. `frontend-modern/src/features/platformPage/usePlatformWindowedItems.ts` shared with `performance-and-scalability`: the platform windowing controller is both a canonical frontend scroll primitive and a directional-runway performance hot path. -9. `frontend-modern/src/routing/routePreload.ts` shared with `performance-and-scalability`, `unified-resources`: the app-shell route preload registry is a canonical frontend shell boundary, an authenticated hot-path performance boundary, and the entry point for the unified-resource Actions workspace. -10. `frontend-modern/src/stores/aiChat.ts` shared with `ai-runtime`: the assistant drawer and session store is both an AI runtime control surface and a canonical app-shell presentation boundary. +13. `frontend-modern/src/routing/routePreload.ts` shared with `performance-and-scalability`, `unified-resources`: the app-shell route preload registry is a canonical frontend shell boundary, an authenticated hot-path performance boundary, and the entry point for the unified-resource Actions workspace. +14. `frontend-modern/src/stores/aiChat.ts` shared with `ai-runtime`: the assistant drawer and session store is both an AI runtime control surface and a canonical app-shell presentation boundary. Assistant session pickers and reloads must restore only safe `handoff_summary` presentation state from the session list. Loading a plain session or starting a new conversation must clear stale scoped handoff @@ -899,7 +898,7 @@ AGENT_SURFACE_ID_PULSE_MCP)` and `getAgentSurfaceToolPosturePresentation`, routes and must render as named choices without secondary raw route IDs, while external provider route IDs may remain visible where they disambiguate catalog entries. -11. `frontend-modern/src/utils/platformSupportManifest.generated.ts` shared with `unified-resources`: the generated platform support projection is both a canonical unified-resource platform union boundary and a shared frontend source/platform vocabulary boundary. +15. `frontend-modern/src/utils/platformSupportManifest.generated.ts` shared with `unified-resources`: the generated platform support projection is both a canonical unified-resource platform union boundary and a shared frontend source/platform vocabulary boundary. It must expose the manifest `surface_kind` field so runtime lenses such as `docker` are not collapsed back into owning platform semantics. It must also preserve canonical projection lists from the governed manifest @@ -907,7 +906,7 @@ AGENT_SURFACE_ID_PULSE_MCP)` and `getAgentSurfaceToolPosturePresentation`, `vm`, `network-share`, and `app-container` workloads through the same generated platform projection used by route helpers, badges, source filters, reportable-resource pickers, and type unions. -12. `frontend-modern/src/utils/sourcePlatforms.ts` shared with `unified-resources`: the source platform normalizer is both a canonical unified-resource source adapter boundary and a shared frontend source/platform vocabulary boundary. +16. `frontend-modern/src/utils/sourcePlatforms.ts` shared with `unified-resources`: the source platform normalizer is both a canonical unified-resource source adapter boundary and a shared frontend source/platform vocabulary boundary. That shared boundary must preserve `availability` as the agentless monitoring source for `network-endpoint` resources and settings presets, so source badges and platform/source type resolution do not fall back to @@ -1247,7 +1246,10 @@ not a replacement status card, CTA band, or page-local nested card. classes, table rendering, and inline close-action chrome must come from `detailSectionModel.ts`, `DetailSectionTable`, and `InlineDetailPanel` instead of local `DetailField` grids or provider-named reusable primitives. - A compact detail row may carry optional bounded progress metadata, but the + A compact detail row may carry optional bounded rich value content for + links, tags, aliases, and address badges while retaining a canonical text + value for titles, tests, and operator-readable fallback. It may also carry + optional bounded progress metadata, but the shared `DetailSectionTable` must render that metadata through the CSP-safe `ProgressBar` while preserving the row's textual value as the primary operator-readable fact. Feature surfaces must omit the metadata when the @@ -3015,10 +3017,10 @@ verification. as `type: 'storage'`, so `topology === 'pool'` is the only pool discriminator available to the page model, and layout strings belong in `storage.vdevLayout`. Regression coverage: the `pool identity - boundary` cases in +boundary` cases in `frontend-modern/src/features/truenas/__tests__/truenasPageModel.test.ts` and `shows the vdev layout as the storage kind while topology stays - the pool discriminator` in +the pool discriminator` in `frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.test.ts`. 43. The diagnostics export sanitizer owns the redaction boundary for the @@ -3029,10 +3031,10 @@ verification. the sanitizer's source, because the failure mode is a payload field added later that the sanitizer never learned about. Regression coverage: `redacts PBS probe failures and state reasons in the - exported bundle` in +exported bundle` in `frontend-modern/src/components/Settings/__tests__/diagnosticsModel.test.ts` and `keeps every PBS diagnostic failure string inside the export - redaction boundary` in +redaction boundary` in `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts`. 44. An org switch must leave every live query surface with data for the @@ -3044,11 +3046,10 @@ verification. refetch shape every other org-switch handler in the app already uses. Stale pre-switch responses are still discarded by request generation. Regression coverage: `refetches a constant-source query after an org - switch` and `does not repopulate the cache when an old-org request - resolves late` in +switch` and `does not repopulate the cache when an old-org request +resolves late` in `frontend-modern/src/hooks/__tests__/createNonSuspendingQuery.test.tsx`. - ## Current State ### Patrol objectives reuse the shared dialog, button, badge, and resource picker contracts @@ -3794,7 +3795,9 @@ section cards on desktop. Desktop cards share the available row width, stretch to the same row height, balance five- and six-section drawers across three-card rows, and use a bounded local label column with left-aligned values so the layout has no ragged fixed-width island, stranded full-width final card, or -full-drawer scan distance. The responsive presentation stays owned by the shared primitive; +full-drawer scan distance. Unified-resource technical summaries are part of +this boundary and must not retain a full-width local table on desktop. The +responsive presentation stays owned by the shared primitive; provider drawers must not fork their own desktop card renderers. Monitoring Optional detail-row progress is also owned by that shared presentation: the value text remains visible, `DetailSectionTable` composes `ProgressBar` for the @@ -6561,7 +6564,7 @@ settings away from every non-admin, and the panel is not empty for them. Proof: which pins the withheld, granted, and unresolved cases plus the `system-general` exclusion. -Because `DEFAULT_SETTINGS_TAB` *is* `infrastructure-systems`, the blocked-route +Because `DEFAULT_SETTINGS_TAB` _is_ `infrastructure-systems`, the blocked-route fallback in `useSettingsAccess.ts` can no longer resolve to the constant — that sent a refused session straight back to the tab that had just refused it. The fallback uses an explicit preference order: the default when reachable, then @@ -6595,7 +6598,8 @@ fail-closed mount, failed-status, deduplicated-load, and General-fallback rules. The Alerts overview may offer a compact per-resource Monitoring menu, but the menu is an adapter over the canonical resource operator-state API. It must -preserve unrelated state on every write, distinguish availability-only +preserve unrelated state on every write, including one-shot/recurring +maintenance and descendant scope, distinguish availability-only expected-offline from all-attention mute, and state that retirement changes Pulse monitoring rather than deleting provider inventory. Resource detail and alert surfaces use the same typed monitoring and lifecycle vocabulary and diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index 0dc18a821..423c8c201 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -2960,7 +2960,12 @@ Monitoring supplies read-only context to the alerts-owned intent resolver. The operator-state adapter resolves source-native references to one canonical unified-resource ID before reading durable operator intent. Lookup failure, ambiguity, absence, or store error yields no suppression context; monitoring -does not synthesize maintenance state. +does not synthesize maintenance state. The adapter may traverse the live +canonical parent chain for maintenance only. An ancestor contributes an active +occurrence only when its scope is `resource_and_descendants`; monitoring mode, +lifecycle state, and every other operator field remain exact-resource policy. +When active windows overlap, the adapter projects the occurrence with the +latest end together with its source id and inherited marker. Backup-aware offline intent consumes a PVE task only when VMID, instance, and node match and the task is active. `pollBackupTasks` stamps server observation @@ -3179,7 +3184,8 @@ change beyond the shutdown defect. ### Monitoring projects canonical resource policy into alert evaluation The monitoring-owned operator-intent adapter projects `monitoringMode` and -`lifecycleState` together with maintenance timing and the legacy compatibility +`lifecycleState` together with effective one-shot/recurring maintenance timing +and the legacy compatibility boolean. It still resolves source-native references through canonical resource identity before reading the store and fails open on missing, ambiguous, or errored identity lookup. Monitoring does not reinterpret provider ownership or @@ -3187,6 +3193,15 @@ invent lifecycle state; Alerts owns signal suppression and unified resources owns persistence. `internal/monitoring/monitor_alert_intent_test.go` and the alerts intent-policy proof pin this adapter boundary. +`internal/maintenancesentinel/` is monitoring-owned post-maintenance assurance. +Its bounded sweep derives every concrete one-shot or recurring occurrence that +ended in the seven-day lookback, de-duplicates on canonical resource plus exact +occurrence end, and writes one maintenance-verification report and timeline +record per occurrence. Restart therefore backfills recent missed recurrences +without mutable scheduler state or duplicate reports; ancient windows remain +out of scope. `internal/maintenancesentinel/sentinel_test.go` and +`verification_test.go` are the focused proof. + ### Agent privilege profile is descriptive model state Host reports may carry an agent-authored privilege profile (effective root, 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 82054b8a4..6c6ad9c5a 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -1404,6 +1404,14 @@ patrol fan-out: in a noisy-warning estate the default policy keeps the LLM-backe investigation path from being invoked once per warning, so the patrol queue and provider spend stay proportional to the alerts the operator actually opted into. +Operator-state mutations wired through `internal/api/router.go` must reconcile +the active-alert set once per mutation so newly inherited parent maintenance +takes effect immediately for every descendant. The reconciliation may inspect +the already resident active-alert map, but it must not scan unified inventory, +query persistence once per resource, or run on the steady-state alert hot path; +ancestor lookup remains bounded by canonical hierarchy depth for each active +alert actually evaluated. + The embedded WorkloadsSurface exposes a `compactGroupHeaders` prop on `frontend-modern/src/components/Workloads/useWorkloadsState.ts` that platform pages owning their own hosts table (Proxmox overview today) set diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index ddf108c6a..d90c300ae 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -5517,6 +5517,7 @@ "owned_prefixes": [ "internal/availabilityprobe/", "internal/fleethealth/", + "internal/maintenancesentinel/", "internal/monitoring/", "internal/storagehealth/", "internal/truenas/", @@ -5566,6 +5567,20 @@ ], "require_explicit_path_policy_coverage": true, "path_policies": [ + { + "id": "maintenance-verification-runtime", + "label": "post-maintenance verification monitoring proof", + "match_prefixes": [ + "internal/maintenancesentinel/" + ], + "match_files": [], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "internal/maintenancesentinel/sentinel_test.go", + "internal/maintenancesentinel/verification_test.go" + ] + }, { "id": "availability-certificate-runtime", "label": "availability and certificate monitoring proof", diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 5dcc80e21..19a1111a8 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -501,6 +501,12 @@ the `white_label` branding entitlement. action lifecycle, but it exposes only typed-proposal capture and gives the orchestrator no autonomy control, command execution, or command-shaped approval path. + Operator-state mutation callbacks wired here may trigger alert + reconciliation across active incidents so descendant-scoped maintenance + takes effect immediately, but they receive only tenant-bound canonical + resource identity and the already authorized mutation result. They must not + expose inventory, operator notes, maintenance reasons, credentials, or + cross-organization state through router callbacks or synchronization events. Automatic action authority is a versioned, one-use admission lease rather than a reusable approval. Tenant mode/license/unlock, capability safety and approval floor, resource allowlist/window/Never state, plan hashes, and the diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 36f413ddb..8296dbd55 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -2876,7 +2876,7 @@ completes. The findings runtime reads operator-set state through the same durable `resource_operator_state` SQLite table on every -new-finding-add. Both the time-bounded maintenance window and the +new-finding-add. One-shot and timezone-aware recurring maintenance windows and the indefinite `IntentionallyOffline` flag persist across restarts; the operator commitment in either form survives without needing a re-entry on startup. The provider adapter returns one projection @@ -2885,7 +2885,7 @@ finding regardless of which signal is active. The `resource_operator_state` SQLite table introduced by the unified-resources store keeps operator-set per-resource intent -(intentionally offline, never auto-remediate, maintenance window, +(intentionally offline, never auto-remediate, one-shot or recurring maintenance window and scope, criticality) durably alongside the rest of the unified-resource durable state. The `/api/resources/{id}/operator-state` API surface in `internal/api/resources_operator_state.go` reads and writes that table diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 5dbb46403..4ff214065 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -120,7 +120,6 @@ NVMe percentage-used conversion is shared with storage risk: negative values remain unknown, while values above 100 clamp to exhausted before remaining life is derived. - `WearoutUnreported` is the canonical absent-value sentinel for `PhysicalDiskMeta.Wearout` and is pinned to `-1`. Views and adapters must return it whenever a resource carries no physical-disk facet; returning the Go zero @@ -142,7 +141,7 @@ about the same disk cannot diverge. 8. `internal/unifiedresources/metrics.go` 9. `internal/unifiedresources/metrics_targets.go` 10. `internal/unifiedresources/registry.go` -10a. `internal/unifiedresources/xcpng.go` + 10a. `internal/unifiedresources/xcpng.go` 11. `internal/unifiedresources/resolve.go` 12. `internal/unifiedresources/resolve_context.go` 13. `internal/unifiedresources/resolved_host_set.go` @@ -159,8 +158,8 @@ about the same disk cannot diverge. 24. `internal/unifiedresources/relationships.go` 25. `internal/unifiedresources/privacy.go` 26. `internal/unifiedresources/actions.go` -26a. `internal/unifiedresources/action_dispatch.go` -26b. `internal/unifiedresources/action_dispatch_store.go` + 26a. `internal/unifiedresources/action_dispatch.go` + 26b. `internal/unifiedresources/action_dispatch_store.go` 27. `internal/unifiedresources/audit_redaction.go` 28. `frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx` 29. `frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx` @@ -169,9 +168,9 @@ about the same disk cannot diverge. 32. `frontend-modern/src/components/Docker/SwarmServicesDrawer.tsx` 33. `frontend-modern/src/features/docker/DockerConfigsTable.tsx` 34. `frontend-modern/src/features/docker/DockerContainersTable.tsx` -34a. `frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx` -34b. `frontend-modern/src/features/docker/dockerContainerLifecycleActions.ts` -34c. `frontend-modern/src/features/docker/dockerContainerTableModel.ts` + 34a. `frontend-modern/src/features/docker/DockerContainerLifecycleControls.tsx` + 34b. `frontend-modern/src/features/docker/dockerContainerLifecycleActions.ts` + 34c. `frontend-modern/src/features/docker/dockerContainerTableModel.ts` 35. `frontend-modern/src/features/docker/DockerImagesTable.tsx` 36. `frontend-modern/src/features/docker/DockerNativeTableShared.tsx` 37. `frontend-modern/src/features/docker/DockerNetworksTable.tsx` @@ -188,7 +187,7 @@ about the same disk cannot diverge. 48. `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` 49. `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` 50. `frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx` -50a. `frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx` + 50a. `frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx` 51. `frontend-modern/src/components/Infrastructure/UnifiedResourceHostTableCard.tsx` 52. `frontend-modern/src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx` 53. `frontend-modern/src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx` @@ -199,7 +198,7 @@ about the same disk cannot diverge. 58. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerServiceModel.ts` 59. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts` 60. `frontend-modern/src/components/Infrastructure/resourceDetailDiscoveryModel.ts` -60a. `frontend-modern/src/utils/workloads.ts` + 60a. `frontend-modern/src/utils/workloads.ts` 61. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerOperationalModel.ts` 62. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerHistoryState.ts` 63. `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts` @@ -209,7 +208,7 @@ about the same disk cannot diverge. 67. `frontend-modern/src/components/Discovery/discoveryReadiness.ts` 68. `frontend-modern/src/components/Discovery/DiscoveryTab.tsx` 69. `frontend-modern/src/components/Discovery/useDiscoveryTabState.ts` -69a. `frontend-modern/src/components/Discovery/useDiscoveryFeatureAvailability.ts` + 69a. `frontend-modern/src/components/Discovery/useDiscoveryFeatureAvailability.ts` 70. `frontend-modern/src/utils/agentResources.ts` 71. `frontend-modern/src/utils/canonicalResourceTypes.ts` 72. `frontend-modern/src/utils/resourceBadgePresentation.ts` @@ -217,77 +216,77 @@ about the same disk cannot diverge. 74. `frontend-modern/src/utils/actionAuditPresentation.ts` 75. `frontend-modern/src/utils/resourceCorrelationPresentation.ts` 76. `frontend-modern/src/utils/resourcePlatformData.ts` -76. `frontend-modern/src/utils/resourcePolicyPresentation.ts` -77. `frontend-modern/src/utils/resourceStateAdapters.ts` -78. `frontend-modern/src/utils/resourceTypeCompat.ts` -79. `frontend-modern/src/utils/resourceTypePresentation.ts` -80. `frontend-modern/src/utils/serviceHealthPresentation.ts` -81. `frontend-modern/src/utils/sourceTypePresentation.ts` -82. `frontend-modern/src/utils/workloadTypePresentation.ts` -83. `frontend-modern/src/utils/resourceIdentity.ts` -84. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerIdentityModel.ts` -85. `frontend-modern/src/hooks/useUnifiedResources.ts` -86. `frontend-modern/src/types/resource.ts` -87. `frontend-modern/src/utils/sourcePlatforms.ts` -88. `frontend-modern/src/utils/platformSupportManifest.generated.ts` -89. `internal/unifiedresources/kubernetes_metric_ids.go` -90. `internal/unifiedresources/policy_posture.go` -91. `frontend-modern/src/features/platformNavigation/platformNavigationModel.ts` -91. `internal/unifiedresources/clone.go` -92. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerPresentation.ts` -93. `internal/unifiedresources/storage_consumers.go` -94. `frontend-modern/src/features/standalone/standalonePageModel.ts` -95. `frontend-modern/src/features/standalone/StandalonePageSurface.tsx` -96. `frontend-modern/src/features/standalone/AgentsMachinesTable.tsx` -97. `frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx` -98. `internal/platformsupport/manifest_generated.go` -99. `frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx` -100. `frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx` -101. `frontend-modern/src/features/kubernetes/kubernetesPageModel.ts` -102. `frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx` -103. `frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx` -104. `frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx` -105. `frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx` -106. `frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx` -107. `frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx` -108. `frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx` -109. `frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx` -110. `frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx` -111. `frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx` -112. `frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx` -113. `frontend-modern/src/features/docker/DockerAlertsTable.tsx` -114. `frontend-modern/src/features/docker/DockerServicesTable.tsx` -115. `frontend-modern/src/features/docker/DockerStorageUsageTable.tsx` -116. `frontend-modern/src/features/actions/ActionDecisionPacket.tsx` -117. `frontend-modern/src/features/actions/ActionReviewDialog.tsx` -118. `frontend-modern/src/features/actions/actionPresentation.ts` -118a. `frontend-modern/src/features/actions/actionRouting.ts` -119. `frontend-modern/src/pages/Actions.tsx` -120. `frontend-modern/src/routing/navigation.ts` -121. `frontend-modern/src/routing/routePreload.ts` -116. `frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx` -117. `frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx` -118. `frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx` -119. `frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx` -120. `frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx` -121. `frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx` -122. `frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx` -122a. `frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts` -122b. `frontend-modern/src/features/proxmox/proxmoxPageModel.ts` -123. `frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx` -124. `frontend-modern/src/features/truenas/TrueNASAppsTable.tsx` -125. `frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx` -126. `frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx` -127. `frontend-modern/src/features/truenas/TrueNASServicesTable.tsx` -128. `frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx` -129. `frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx` -130. `frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx` -131. `frontend-modern/src/features/vmware/VsphereActivityTable.tsx` -132. `frontend-modern/src/features/vmware/VsphereAlertsTable.tsx` -133. `frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx` -134. `frontend-modern/src/features/vmware/VsphereNetworksTable.tsx` -135. `frontend-modern/src/features/truenas/TrueNASPageSurface.tsx` -136. `frontend-modern/src/features/vmware/VmwarePageSurface.tsx` +77. `frontend-modern/src/utils/resourcePolicyPresentation.ts` +78. `frontend-modern/src/utils/resourceStateAdapters.ts` +79. `frontend-modern/src/utils/resourceTypeCompat.ts` +80. `frontend-modern/src/utils/resourceTypePresentation.ts` +81. `frontend-modern/src/utils/serviceHealthPresentation.ts` +82. `frontend-modern/src/utils/sourceTypePresentation.ts` +83. `frontend-modern/src/utils/workloadTypePresentation.ts` +84. `frontend-modern/src/utils/resourceIdentity.ts` +85. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerIdentityModel.ts` +86. `frontend-modern/src/hooks/useUnifiedResources.ts` +87. `frontend-modern/src/types/resource.ts` +88. `frontend-modern/src/utils/sourcePlatforms.ts` +89. `frontend-modern/src/utils/platformSupportManifest.generated.ts` +90. `internal/unifiedresources/kubernetes_metric_ids.go` +91. `internal/unifiedresources/policy_posture.go` +92. `frontend-modern/src/features/platformNavigation/platformNavigationModel.ts` +93. `internal/unifiedresources/clone.go` +94. `frontend-modern/src/components/Infrastructure/resourceDetailDrawerPresentation.ts` +95. `internal/unifiedresources/storage_consumers.go` +96. `frontend-modern/src/features/standalone/standalonePageModel.ts` +97. `frontend-modern/src/features/standalone/StandalonePageSurface.tsx` +98. `frontend-modern/src/features/standalone/AgentsMachinesTable.tsx` +99. `frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx` +100. `internal/platformsupport/manifest_generated.go` +101. `frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx` +102. `frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx` +103. `frontend-modern/src/features/kubernetes/kubernetesPageModel.ts` +104. `frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx` +105. `frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx` +106. `frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx` +107. `frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx` +108. `frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx` +109. `frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx` +110. `frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx` +111. `frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx` +112. `frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx` +113. `frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx` +114. `frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx` +115. `frontend-modern/src/features/docker/DockerAlertsTable.tsx` +116. `frontend-modern/src/features/docker/DockerServicesTable.tsx` +117. `frontend-modern/src/features/docker/DockerStorageUsageTable.tsx` +118. `frontend-modern/src/features/actions/ActionDecisionPacket.tsx` +119. `frontend-modern/src/features/actions/ActionReviewDialog.tsx` +120. `frontend-modern/src/features/actions/actionPresentation.ts` + 118a. `frontend-modern/src/features/actions/actionRouting.ts` +121. `frontend-modern/src/pages/Actions.tsx` +122. `frontend-modern/src/routing/navigation.ts` +123. `frontend-modern/src/routing/routePreload.ts` +124. `frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx` +125. `frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx` +126. `frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx` +127. `frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx` +128. `frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx` +129. `frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx` +130. `frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx` + 122a. `frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts` + 122b. `frontend-modern/src/features/proxmox/proxmoxPageModel.ts` +131. `frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx` +132. `frontend-modern/src/features/truenas/TrueNASAppsTable.tsx` +133. `frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx` +134. `frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx` +135. `frontend-modern/src/features/truenas/TrueNASServicesTable.tsx` +136. `frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx` +137. `frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx` +138. `frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx` +139. `frontend-modern/src/features/vmware/VsphereActivityTable.tsx` +140. `frontend-modern/src/features/vmware/VsphereAlertsTable.tsx` +141. `frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx` +142. `frontend-modern/src/features/vmware/VsphereNetworksTable.tsx` +143. `frontend-modern/src/features/truenas/TrueNASPageSurface.tsx` +144. `frontend-modern/src/features/vmware/VmwarePageSurface.tsx` ## Shared Boundaries @@ -845,12 +844,12 @@ container inventory table. 14. `frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx` shared with `storage-recovery`: Proxmox recoverable workload table rows are both a storage/recovery coverage surface and a unified-resource platform-table consumer boundary. 15. `frontend-modern/src/routing/routePreload.ts` shared with `frontend-primitives`, `performance-and-scalability`: the app-shell route preload registry is a canonical frontend shell boundary, an authenticated hot-path performance boundary, and the entry point for the unified-resource Actions workspace. 16. `frontend-modern/src/stores/websocket-global.ts` shared with `performance-and-scalability`: the process-wide realtime store owner is both a unified-resource state boundary and a fleet-scale connection and reconciliation hot path. -16. `frontend-modern/src/utils/platformSupportManifest.generated.ts` shared with `frontend-primitives`: the generated platform support projection is both a canonical unified-resource platform union boundary and a shared frontend source/platform vocabulary boundary. +17. `frontend-modern/src/utils/platformSupportManifest.generated.ts` shared with `frontend-primitives`: the generated platform support projection is both a canonical unified-resource platform union boundary and a shared frontend source/platform vocabulary boundary. It must carry the manifest `surface_kind` distinction so `docker` remains machine-readable as a `runtime-lens` while owning infrastructure sources remain `platform` entries. -17. `frontend-modern/src/utils/resourceStateAdapters.ts` shared with `performance-and-scalability`: canonical resource compatibility and host coalescence are both a unified-resource contract and a fleet-scale reconciliation hot path. -18. `frontend-modern/src/utils/sourcePlatforms.ts` shared with `frontend-primitives`: the source platform normalizer is both a canonical unified-resource source adapter boundary and a shared frontend source/platform vocabulary boundary. +18. `frontend-modern/src/utils/resourceStateAdapters.ts` shared with `performance-and-scalability`: canonical resource compatibility and host coalescence are both a unified-resource contract and a fleet-scale reconciliation hot path. +19. `frontend-modern/src/utils/sourcePlatforms.ts` shared with `frontend-primitives`: the source platform normalizer is both a canonical unified-resource source adapter boundary and a shared frontend source/platform vocabulary boundary. That shared vocabulary boundary owns the generic `docker` platform label: selectors, badges, and filter options render it as "Docker / Podman" so v5 Docker users can still find the runtime surface while Podman-backed @@ -872,8 +871,8 @@ container inventory table. display/source family; `platformScopes` is the overlap set used when a runtime workload belongs to both Docker and an owning infrastructure platform. -19. `frontend-modern/src/utils/workloads.ts` shared with `performance-and-scalability`: the stable workload metadata identity helper is both a unified-resource persistence boundary and a workloads hot-path lookup boundary. -20. `internal/api/resourceapi/resources.go` shared with `api-contracts`: the unified resource endpoint is both a backend payload contract surface and a unified-resource runtime boundary. +20. `frontend-modern/src/utils/workloads.ts` shared with `performance-and-scalability`: the stable workload metadata identity helper is both a unified-resource persistence boundary and a workloads hot-path lookup boundary. +21. `internal/api/resourceapi/resources.go` shared with `api-contracts`: the unified resource endpoint is both a backend payload contract surface and a unified-resource runtime boundary. `/api/resources` type filters must accept URL-encoded comma-separated lists from browser query builders exactly like literal comma separators, so Docker / Podman runtime pages do not lose `docker-host` inventory while requesting @@ -887,12 +886,13 @@ container inventory table. id over host labels when building host-level Discovery targets, so detail drawers, websocket hydration, and API lookups use the same identity. The global resource timeline is also owned at this boundary. `GET - /api/resources/timeline` may expose provider-wide `ResourceChange` records +/api/resources/timeline` may expose provider-wide `ResourceChange` records for platform pages before a single resource drawer is selected, but those records must still come from the canonical resource-change store and use the same filter parser as per-resource timelines. Relationship-aware expansion remains a per-resource timeline behavior; unscoped provider activity must not infer related resources in the frontend. + ## Extension Points The global Product Trust projection is owned at @@ -1128,9 +1128,9 @@ cannot create a browser mutation. tables must not render ConfigMap or Secret payload values, and metadata-only rows must not expose key names as if payload fields had been read. 2. Add typed accessors and views in `internal/unifiedresources/views.go` -Resource detail mappers now reuse the shared -`frontend-modern/src/utils/textPresentation.ts` title-case helper for sensor -labels so the canonical unified-resource presentation layer owns the wording. + Resource detail mappers now reuse the shared + `frontend-modern/src/utils/textPresentation.ts` title-case helper for sensor + labels so the canonical unified-resource presentation layer owns the wording. The canonical AI-safe summary builder now owns the sensitivity-specific suffix phrases for `sensitive` and `restricted` resources, so the backend policy @@ -1148,350 +1148,342 @@ Canonical policy posture aggregation is owned here as well. Resource API payloads may expose a camelCase transport projection, but the counts must be derived from `internal/unifiedresources/policy_posture.go` after canonical policy metadata has been refreshed, not recomputed from frontend labels, -AI-only summary payloads, or page-local heuristics. -4. Add metrics-target normalization, surface-friendly projections of - nested source payloads, or synthetic metrics support through - `internal/unifiedresources/metrics_targets.go`, - `internal/unifiedresources/metrics.go`, and the relevant adapter in - `internal/unifiedresources/adapters.go`. The unified `Resource` shape - carries top-level `Uptime` and `Temperature` projections so frontend - tables that render those columns do not have to dig into per-source - payloads (`agent.uptimeSeconds`, `proxmox.uptime`, - `agent.temperature`, `proxmox.temperature`); adapters that wrap an - `AgentData` or `ProxmoxData` must populate those top-level fields - from the nested source values, and adapters for resource types that - have no native uptime/temperature concept (e.g. `k8s-deployment`, - `k8s-replicaset`, `k8s-configmap`, `k8s-secret`, - `docker-service`, `k8s-cluster` aggregates) must leave them unset so - bespoke platform-page tables can hide the column entirely. - Kubernetes deployment metrics live on the canonical adapter through - `metricsFromKubernetesDeployment(cluster, deployment)`. Upstream - Deployments do not expose CPU / memory natively because they are - scheduling abstractions over their controlled pods, so the helper - returns nil for non-mock runtimes (until the adapter aggregates pod - metrics into the owning deployment) and synthesizes deployment-stable - CPU / memory / disk / network values for mock mode so the - platform-page Deployments table renders meaningful operator values - instead of dashes. The synthetic branch is gated by - `mockmode.IsEnabled()` and scales with the deployment's - ready/desired/available replica state so degraded deployments read as - elevated pressure on the surviving replicas. - Namespaced Kubernetes adapters share one Resource scaffold: a new - namespaced kind populates its kind-specific `K8sData` fields (after - `baseKubernetesData`) and delegates Resource assembly and identity to - `namespacedKubernetesResource(cluster, clusterName, namespace, name, +AI-only summary payloads, or page-local heuristics. 4. Add metrics-target normalization, surface-friendly projections of +nested source payloads, or synthetic metrics support through +`internal/unifiedresources/metrics_targets.go`, +`internal/unifiedresources/metrics.go`, and the relevant adapter in +`internal/unifiedresources/adapters.go`. The unified `Resource` shape +carries top-level `Uptime` and `Temperature` projections so frontend +tables that render those columns do not have to dig into per-source +payloads (`agent.uptimeSeconds`, `proxmox.uptime`, +`agent.temperature`, `proxmox.temperature`); adapters that wrap an +`AgentData` or `ProxmoxData` must populate those top-level fields +from the nested source values, and adapters for resource types that +have no native uptime/temperature concept (e.g. `k8s-deployment`, +`k8s-replicaset`, `k8s-configmap`, `k8s-secret`, +`docker-service`, `k8s-cluster` aggregates) must leave them unset so +bespoke platform-page tables can hide the column entirely. +Kubernetes deployment metrics live on the canonical adapter through +`metricsFromKubernetesDeployment(cluster, deployment)`. Upstream +Deployments do not expose CPU / memory natively because they are +scheduling abstractions over their controlled pods, so the helper +returns nil for non-mock runtimes (until the adapter aggregates pod +metrics into the owning deployment) and synthesizes deployment-stable +CPU / memory / disk / network values for mock mode so the +platform-page Deployments table renders meaningful operator values +instead of dashes. The synthetic branch is gated by +`mockmode.IsEnabled()` and scales with the deployment's +ready/desired/available replica state so degraded deployments read as +elevated pressure on the surviving replicas. +Namespaced Kubernetes adapters share one Resource scaffold: a new +namespaced kind populates its kind-specific `K8sData` fields (after +`baseKubernetesData`) and delegates Resource assembly and identity to +`namespacedKubernetesResource(cluster, clusterName, namespace, name, resourceType, status, data, labels)` in - `internal/unifiedresources/adapters.go` instead of hand-rolling the - `Resource{...}` literal plus `namespacedKubernetesIdentity` return. - The scaffold owns `Technology: "kubernetes"`, `LastSeen` from the - cluster, `UpdatedAt`, the `Kubernetes` facet pointer, and label-derived - tags; only cluster-scoped or non-namespaced kinds (cluster, node, PV, - StorageClass, namespace itself) keep bespoke identity construction. -5. Add platform registry, resolution, host-dedup, or monitored-system - projection behavior through `internal/unifiedresources/registry.go`, - `internal/unifiedresources/resolve.go`, - `internal/unifiedresources/resolved_host_set.go`, - `internal/unifiedresources/snapshot_source_filter.go`, - `internal/unifiedresources/store.go`, - `internal/unifiedresources/kubernetes_capabilities.go`, - `internal/unifiedresources/pbs_rollups.go`, - `internal/unifiedresources/monitored_systems.go`, - `internal/unifiedresources/monitored_system_projection.go`, and - the shared list-order helpers consumed by `internal/api/resourceapi/resources.go`; - canonical unified-resource lists must preserve one deterministic - `name -> type -> id` order across registry reads, REST pagination, and - websocket-backed refreshes so equal-name resources do not silently reshuffle - between cold hydrate and later runtime updates - Realtime delta reconciliation must preserve exact display-object identity - for untouched non-host resources, canonicalize changed and newly added rows, - and re-evaluate exactly the host-merge groups the delta could have altered: - a group refreshes when a flagged id names one of its current members, and a - flagged id absent from the incoming snapshot (a removal, or a partner id an - earlier coalesce folded away) conservatively refreshes every group. A tick - that flags no member of a group must preserve that group's cached merged - host row by object identity. Incremental and - full-snapshot paths must therefore produce the same canonical host identity, - labels, and compatibility fields without cloning the entire estate per tick. - The connection store publishes each reconciliation's changed IDs and resource - revision. `useUnifiedResources` applies that revision to the shared - all-resources cache once and derives type-filtered route projections from the - canonical result. An instance observing a revision the shared cache already - holds must not re-read or deep-unwrap the realtime store, and the merging - instance dereferences raw store subtrees only for rows the delta merge will - clone (flagged ids, host-merge members, and ids absent from the shared - cache). A sequential revision with unchanged route membership - patches only the changed row indices plus the bounded agent coalescing set. - The connection store retains a bounded per-revision changed-id history; an - instance that resumes several revisions behind the shared cache must catch - up through the unioned changed-id set as an incremental delta merge whenever - the history covers the gap, so tab entry and re-entry do not deep-unwrap or - remerge the full estate. Only initial hydration, uncovered revision gaps, - full-snapshot commits, additions, removals, or reorderings - fall back to keyed full reconciliation. - Each reconciliation also records the per-resource top-level keys its merge - patches touched (`platformData` expanded one level), published with the - revision and unioned across the history window and the hidden-tab deferral - set with unknown-shape contamination. A changed non-host row whose recorded - keys stay within the pass-through metric fields, the `proxmox` facet - mirror, and the `platformData` metric mirror leaves takes a fast merge - path: the previous display row with only the patched subtrees cloned in, - bypassing the full clone-canonicalize-merge, and committing to the - connection store and instance projections as per-key subtree writes rather - than whole-row keyed reconciles. The fast output must stay - content-equivalent to the full path (facet keeps, deletion semantics, and - default-policy synthesis included), must never adopt raw-baseline subtrees - by reference, and any row outside the allow-list — including agent rows, - whose output can depend on host coalescing — must take the full path. Route-prefetch and route-realtime - activation are separate: - a prefetched hidden surface may retain REST data without subscribing its full - projection to every realtime tick, and activation catches up from the shared - cache. Richer REST-only facets are promoted into that cache before thinner - realtime deltas are applied, so the optimization cannot discard disk I/O, - PBS, policy, or provider metadata. - Broadcast payload slimming is reversed at the connection-store ingestion - boundary, before any canonical merge or consumer read: `capabilitiesRef` is - expanded into per-row inline `capabilities` through the state payload's - `capabilityCatalog` (per-row clones, because store reconciliation mutates - adopted objects in place), and a resource arriving without a policy is given - a synthesized default posture (internal sensitivity, cloud-summary routing, - no redactions) so a posture transition patched as `policy: null` cannot - leave a stale governed policy behind. Client identity-alias resolution must - consult `canonicalIdentity.supersededIds` explicitly, because broadcast - aliases no longer duplicate superseded canonical ids. - That same unified-resource owner also defines the canonical transport - projection for operator-facing resources: `/api/resources` and websocket - `state.resources` must share `ContractResourceType`, canonical display - names, and canonical cluster labels instead of publishing separate REST and - broadcast aliases for the same machine. - Fleet command posture that reaches resource-facing rows must remain a - projection of `/api/connections` `fleet.commandPolicy`: desired server - policy, applied agent truth, enforcement, and reason stay separate. Unified - resource consumers may show compact remote-control status, but they must not - treat top-level `remoteControl` as applied agent runtime truth, and they - must preserve desired/applied drift or no-report attention when enriching - resource rows. - Platform-page stale-agent notices may consume canonical agent identity from - merged resources only to scope the Infrastructure settings update-command - route to the affected agents. That scoped lifecycle handoff must not become a - new resource-action authority, a page-local command runner, or a substitute - for the `/api/connections` fleet command-policy truth described above. - Resource consumers must also use the API-owned agent update target when - comparing resource-carried agent versions; the running app build version is - not a resource freshness contract. - Kubernetes node rows are cluster-agent-backed for this purpose: even when a - canonical `k8s-node` row is a pure Kubernetes API projection with no merged - `agent` facet, `internal/unifiedresources/adapters.go` must carry the - cluster `AgentID` and cluster-scoped `AgentVersion` on the row's - Kubernetes facet so platform consumers can scope stale-agent notices and - update-command links from typed resource evidence instead of rebuilding - ownership from the parent cluster row. - `internal/unifiedresources/top_level_systems.go` - Explicit linked-host correlation is canonical here: when Kubernetes node - ingest has a resolved backing host agent, the registry must merge that node - into the agent resource instead of publishing duplicate top-level - infrastructure rows for the same machine under both `agent` and `k8s-node` - identities. - Canonical read-state overlays belong here as well: when monitoring or a - preview path needs to project extra source-native records onto an existing - settled read state, it must do so through - `internal/unifiedresources/monitor_adapter.go` and - `internal/unifiedresources/registry.go` so matcher seeding, manual links, - and merge semantics stay unified-resource-owned instead of being rebuilt in - consumers. - Storage consumer projection is unified-resource-owned through - `internal/unifiedresources/storage_consumers.go`. When a provider publishes - source-native storage consumer metadata that cannot be derived from shared - Proxmox/PBS relationship indexes, refresh must preserve that source-owned - consumer count, consumer type list, and top-consumer summary on the - canonical storage resource unless a stronger shared consumer projection has - already populated those fields in the same refresh. - Operator-facing storage posture wording is part of that same ownership: - when multiple storage-risk reasons exist, shared posture helpers must prefer - the most decision-useful protection loss summary such as lost parity over a - generic disk-count aggregate, so resource drawers and incidents do not hide - the actual protection boundary behind a broader count phrase. -6. Add canonical governed name-resolution or policy-aware resource lookup behavior through `internal/unifiedresources/resolve.go` and `internal/unifiedresources/resolve_context.go` -8. Add or change discovery-support runtime under the resource drawer through `frontend-modern/src/components/Discovery/DiscoveryTab.tsx` for shell/presentation ownership, `frontend-modern/src/components/Discovery/useDiscoveryTabState.ts` for fetch, websocket-progress, manual-run triggering, and notes-mutation ownership, and `frontend-modern/src/components/Discovery/discoveryReadiness.ts` for the shared readiness verdict used by resource-drawer Discovery surfaces. Embedded drawers may expose the top-level run action through this shared Discovery tab, but they must still call the canonical discovery trigger state path instead of introducing drawer-local API mutations. - Drawer-level feature availability belongs to - `frontend-modern/src/components/Discovery/useDiscoveryFeatureAvailability.ts`. - It consumes the shared AI runtime settings store and fails closed until the - runtime explicitly reports `discovery_enabled=true`. Resource, guest, node, - and Docker host drawers must use that boundary for every Discovery tab, - readiness badge, analysis reveal, identified-service suggestion, and - passive discovery-record query. A disabled or unresolved feature must leave - no Discovery mention in drawer chrome or content and must not start a - drawer-local discovery read. - Resource drawer secondary sections, action history, discovery run summaries, - and other compact resource-detail cards may own their resource-specific - labels, rows, filters, and actions, but the repeated bordered compact frame - is a frontend-primitives boundary. `ResourceDetailDrawerOverviewTab.tsx`, - `ResourceActionHistory.tsx`, and `DiscoveryTab.tsx` must compose - `InfoCardFrame` for that shell instead of restoring local card-frame - classes. - Curated technical inventory follows the shared compact-row contract instead - of the secondary-card contract. Docker-host drawers must project system, - runtime, memory, storage, and telemetry facts through - `TechnicalDetailsSection` with canonical `DetailSection[]` data, while the - unified-resource drawer keeps its existing compact technical summary tables - visible. Only genuinely large or interactive provider-support content stays - lazy behind `TechnicalDetailsDisclosure`; technical inventory must not - restore a local card mosaic or add a drawer-open fetch. -9. Keep dashboard and infrastructure freshness on the canonical unified-resource - ownership path. `frontend-modern/src/stores/websocket.ts`, - `frontend-modern/src/utils/resourceStateAdapters.ts`, and - `frontend-modern/src/hooks/useUnifiedResources.ts` together own the frontend - canonicalization boundary: REST may hydrate the initial snapshot and - unsupported filtered queries, but supported snapshot freshness must come - from websocket `state.resources` instead of layering confirmatory - route-local REST refetch loops over already-owned resource - updates. - Oversized WebSocket recovery is the transport exception: a complete - `/api/state` response may refresh display state while the connection remains - baseline-free, but resource deltas must not patch that independently built - REST snapshot. The store resumes delta application only after the same - connection delivers a complete WebSocket resource snapshot. - Browser WebSocket liveness tracking is part of that same store boundary: - valid inbound server messages, including heartbeat `ping`/`pong` traffic, - must refresh the browser-side activity timestamp so quiet periods between - resource snapshots do not cause avoidable reconnect churn. - That shared store/adapter/hook path must also preserve canonical row shape - across transport boundaries: thinner realtime `state.resources` payloads - must merge into the existing canonical resource snapshot instead of - downgrading richer REST-only infrastructure details such as disk I/O, source - metadata, or platform summary fields after first hydrate. For default - Source lists and their source-specific facets are the exception: a current - snapshot with canonical source evidence replaces stale source lists and - removes provider facets that no longer have matching source evidence, so - rows do not keep displaying a previous platform identity after websocket - refreshes. For default - `initialHydration: 'immediate'` consumers, that same path must not paint the - thinner websocket transport before the first canonical REST snapshot exists; - only explicit websocket-first consumers may render directly from the realtime - transport before canonical hydrate completes. Operator surfaces that must - preserve already-known infrastructure continuity after login, such as the - Infrastructure page, must use websocket-first hydration with stale-cache - REST revalidation after the first-paint settle window so the page can paint - from live state immediately without forcing a second resource-shape - transition while summary and table surfaces are still mounting. - Org-scope and enabled-state transitions in - `frontend-modern/src/hooks/useUnifiedResources.ts` must invalidate older - in-flight REST refreshes before publishing the new scoped cache entry, so a - stale request cannot set active-scope errors, clear the active request guard, - or replace the currently mounted Infrastructure/Workloads resource snapshot. - Canonical cluster membership in that shared path must come only from - explicit cluster identity such as Kubernetes context or platform cluster - labels; standalone resource names must never be repurposed as synthetic - `clusterId` values. -13. Keep operator-facing resource analysis vocabulary task-first on unified-resource - surfaces. `frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx`, - `frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts`, - and `frontend-modern/src/components/Discovery/DiscoveryTab.tsx` may expose - provider identity or governed safe-summary posture when that context helps - an operator, but the rendered labels must stay product-neutral and use - `Analysis`, `Analysis Reasoning`, and `Safe Summary` rather than reviving - generic `AI` or `AI-Safe` branding inside the resource drawer or discovery - shell. -14. Keep the operator-facing unified resource table width-aware at the table - surface, not just at the browser viewport. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx` - must route its root ref through `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`, - and `frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts` - owns the column-priority breakpoints for host and service infrastructure - rows. When the app shell leaves tablet-sized space during live resize, the - table hides lower-priority metadata first. At phone width, the state model - must remove the old 640-pixel floor, preserve identity at exactly 30 percent - of the table, and allocate the remaining width across the bounded - source-relevant health and activity columns. Both the document and table - shell must remain free of horizontal overflow; desktop and tablet stages - retain their existing complete column contracts. -15. Keep shared policy-posture framing on the unified-resource card owner. - `frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx` - may accept caller-owned subtitle or resource-count wording when Patrol or - another shared surface needs to explain how the same governed policy counts - should be read, but those framing lines must extend the shared card API - rather than spawning page-local policy summary shells. -16. Keep platform/runtime top-level route paths on the canonical resource-link - helper. `frontend-modern/src/routing/resourceLinks.ts` owns the - `STANDALONE_PATH`, `DOCKER_PATH`, `KUBERNETES_PATH`, `TRUENAS_PATH`, - `VMWARE_PATH`, `PATROL_PATH`, `PATROL_CONTROL_ANCHOR`, - `PATROL_CONTROL_PATH`, and `PATROL_CONTROL_STARTER_QUERY_PARAM` constants, - the route-backed Patrol control starter helpers, and the `buildStandalonePath`, - `buildDockerPath`, `buildKubernetesPath`, `buildTrueNASPath`, - `buildVmwarePath` builders. - Per-platform surfaces and tab specs must - derive every internal link from those builders so the canonical resource - URL vocabulary stays single-sourced; ad hoc string concatenation of - platform routes inside feature directories is not permitted. The canonical - Pulse Intelligence external-agent hash - `/settings/pulse-intelligence/assistant#external-agent-setup` and legacy - `/settings/security/api#external-agent-setup` / - `/settings/security/api#pulse-mcp-setup` compatibility hashes may live in - the shared route helper, but they are adjacent settings route state, not - unified-resource identity, platform scope, or drawer focus state. - The Patrol `patrolControlStarter=patrol_control` query is an adjacent - first-party Patrol control handoff flag, with legacy - `operationsLoopStarter` values accepted only as compatibility aliases, not - unified-resource filters or focus keys. Unified-resource consumers must not reuse those values for resource - identity, list filtering, contextual focus, storage state, recovery state, - or platform scoping. - The user-facing Machines surface's default resource route is the machines projection - (`/standalone/machines`); agentless endpoint rows use the - `/standalone/availability` projection and must not be collapsed into a - generic overview URL. - The frontend-primitives-owned Machines IA contract consumes the - unified-resource projection for Pulse-managed standalone agent rows and - agentless availability endpoint rows; this subsystem owns only the - membership rules for those projected rows. Agent membership must require - `resource.type === "agent"`, canonical Pulse-agent source evidence from - resource sources or source status, and no stronger provider-owner evidence - from Proxmox, VMware, TrueNAS, or Kubernetes. Source-less legacy snapshots - may fall back to a normalized `platformType === "agent"`, but - provider-owned nodes must not become machine-page members through - hostname, `agent` platform scope, or agent telemetry alone; those facts - surface as facets on the owning provider page. - `AgentsMachinesTable.tsx` may own row membership, resource-derived menu - eligibility, evidence-gated column relevance, and remove-agent semantics - for these projected rows. In particular, the GPU metric column and its - View choice are relevant only when at least one projected machine reports - finite GPU utilization; absent telemetry must not produce an empty default - column or a no-op column choice, while persisted visibility remains ready - for the column when evidence later appears. The - compact row action trigger chrome stays under the frontend-primitives - `ActionIconButton` boundary rather than becoming a unified-resource-local - button shell. - Machines list search and online-state narrowing are frontend route state, - not new unified-resource membership fields. `StandalonePageSurface.tsx` - owns the `STANDALONE_QUERY_PARAMS` query/status projection and one composite - reset, while `AgentsMachinesTable.tsx` consumes those controlled values so - saved links and bookmarks cannot diverge from the canonical projected row - set. Those query parameters must only narrow the already-owned agent - projection; they must not cause provider nodes or availability endpoints to - enter the Machines membership bucket. - `PROXMOX_BACKUPS_QUERY_PARAMS` in the same shared route-helper module is - storage/recovery-owned filter and workspace state for the Proxmox Backups - surface. Its query, view, node, type, source, posture, and day keys may - narrow already-correlated backup rows, but they do not add unified-resource - membership, change canonical workload identity, or turn recovery evidence - into a provider-resource projection. - The Proxmox backup workspace's chronological and coverage views are - canonical route state under `/proxmox/backups/date` and - `/proxmox/backups/coverage`. The shared route helper owns those path - builders; the backup surface must use the shared platform section-tab - primitive for navigation and may retain legacy query parsing only as - compatibility input. Those view paths describe backup evidence and must - not be treated as unified-resource membership or workload identity. - The default tab for each platform path must point at a sub-tab whose - canonical unified-resource projection actually populates, and visible - workflow subtabs must stay evidence-gated by the same canonical row or - signal source instead of advertising empty object browsers. The - canonical TrueNAS adapter (`internal/truenas/provider.go:: +`internal/unifiedresources/adapters.go` instead of hand-rolling the +`Resource{...}` literal plus `namespacedKubernetesIdentity` return. +The scaffold owns `Technology: "kubernetes"`, `LastSeen` from the +cluster, `UpdatedAt`, the `Kubernetes` facet pointer, and label-derived +tags; only cluster-scoped or non-namespaced kinds (cluster, node, PV, +StorageClass, namespace itself) keep bespoke identity construction. 5. Add platform registry, resolution, host-dedup, or monitored-system +projection behavior through `internal/unifiedresources/registry.go`, +`internal/unifiedresources/resolve.go`, +`internal/unifiedresources/resolved_host_set.go`, +`internal/unifiedresources/snapshot_source_filter.go`, +`internal/unifiedresources/store.go`, +`internal/unifiedresources/kubernetes_capabilities.go`, +`internal/unifiedresources/pbs_rollups.go`, +`internal/unifiedresources/monitored_systems.go`, +`internal/unifiedresources/monitored_system_projection.go`, and +the shared list-order helpers consumed by `internal/api/resourceapi/resources.go`; +canonical unified-resource lists must preserve one deterministic +`name -> type -> id` order across registry reads, REST pagination, and +websocket-backed refreshes so equal-name resources do not silently reshuffle +between cold hydrate and later runtime updates +Realtime delta reconciliation must preserve exact display-object identity +for untouched non-host resources, canonicalize changed and newly added rows, +and re-evaluate exactly the host-merge groups the delta could have altered: +a group refreshes when a flagged id names one of its current members, and a +flagged id absent from the incoming snapshot (a removal, or a partner id an +earlier coalesce folded away) conservatively refreshes every group. A tick +that flags no member of a group must preserve that group's cached merged +host row by object identity. Incremental and +full-snapshot paths must therefore produce the same canonical host identity, +labels, and compatibility fields without cloning the entire estate per tick. +The connection store publishes each reconciliation's changed IDs and resource +revision. `useUnifiedResources` applies that revision to the shared +all-resources cache once and derives type-filtered route projections from the +canonical result. An instance observing a revision the shared cache already +holds must not re-read or deep-unwrap the realtime store, and the merging +instance dereferences raw store subtrees only for rows the delta merge will +clone (flagged ids, host-merge members, and ids absent from the shared +cache). A sequential revision with unchanged route membership +patches only the changed row indices plus the bounded agent coalescing set. +The connection store retains a bounded per-revision changed-id history; an +instance that resumes several revisions behind the shared cache must catch +up through the unioned changed-id set as an incremental delta merge whenever +the history covers the gap, so tab entry and re-entry do not deep-unwrap or +remerge the full estate. Only initial hydration, uncovered revision gaps, +full-snapshot commits, additions, removals, or reorderings +fall back to keyed full reconciliation. +Each reconciliation also records the per-resource top-level keys its merge +patches touched (`platformData` expanded one level), published with the +revision and unioned across the history window and the hidden-tab deferral +set with unknown-shape contamination. A changed non-host row whose recorded +keys stay within the pass-through metric fields, the `proxmox` facet +mirror, and the `platformData` metric mirror leaves takes a fast merge +path: the previous display row with only the patched subtrees cloned in, +bypassing the full clone-canonicalize-merge, and committing to the +connection store and instance projections as per-key subtree writes rather +than whole-row keyed reconciles. The fast output must stay +content-equivalent to the full path (facet keeps, deletion semantics, and +default-policy synthesis included), must never adopt raw-baseline subtrees +by reference, and any row outside the allow-list — including agent rows, +whose output can depend on host coalescing — must take the full path. Route-prefetch and route-realtime +activation are separate: +a prefetched hidden surface may retain REST data without subscribing its full +projection to every realtime tick, and activation catches up from the shared +cache. Richer REST-only facets are promoted into that cache before thinner +realtime deltas are applied, so the optimization cannot discard disk I/O, +PBS, policy, or provider metadata. +Broadcast payload slimming is reversed at the connection-store ingestion +boundary, before any canonical merge or consumer read: `capabilitiesRef` is +expanded into per-row inline `capabilities` through the state payload's +`capabilityCatalog` (per-row clones, because store reconciliation mutates +adopted objects in place), and a resource arriving without a policy is given +a synthesized default posture (internal sensitivity, cloud-summary routing, +no redactions) so a posture transition patched as `policy: null` cannot +leave a stale governed policy behind. Client identity-alias resolution must +consult `canonicalIdentity.supersededIds` explicitly, because broadcast +aliases no longer duplicate superseded canonical ids. +That same unified-resource owner also defines the canonical transport +projection for operator-facing resources: `/api/resources` and websocket +`state.resources` must share `ContractResourceType`, canonical display +names, and canonical cluster labels instead of publishing separate REST and +broadcast aliases for the same machine. +Fleet command posture that reaches resource-facing rows must remain a +projection of `/api/connections` `fleet.commandPolicy`: desired server +policy, applied agent truth, enforcement, and reason stay separate. Unified +resource consumers may show compact remote-control status, but they must not +treat top-level `remoteControl` as applied agent runtime truth, and they +must preserve desired/applied drift or no-report attention when enriching +resource rows. +Platform-page stale-agent notices may consume canonical agent identity from +merged resources only to scope the Infrastructure settings update-command +route to the affected agents. That scoped lifecycle handoff must not become a +new resource-action authority, a page-local command runner, or a substitute +for the `/api/connections` fleet command-policy truth described above. +Resource consumers must also use the API-owned agent update target when +comparing resource-carried agent versions; the running app build version is +not a resource freshness contract. +Kubernetes node rows are cluster-agent-backed for this purpose: even when a +canonical `k8s-node` row is a pure Kubernetes API projection with no merged +`agent` facet, `internal/unifiedresources/adapters.go` must carry the +cluster `AgentID` and cluster-scoped `AgentVersion` on the row's +Kubernetes facet so platform consumers can scope stale-agent notices and +update-command links from typed resource evidence instead of rebuilding +ownership from the parent cluster row. +`internal/unifiedresources/top_level_systems.go` +Explicit linked-host correlation is canonical here: when Kubernetes node +ingest has a resolved backing host agent, the registry must merge that node +into the agent resource instead of publishing duplicate top-level +infrastructure rows for the same machine under both `agent` and `k8s-node` +identities. +Canonical read-state overlays belong here as well: when monitoring or a +preview path needs to project extra source-native records onto an existing +settled read state, it must do so through +`internal/unifiedresources/monitor_adapter.go` and +`internal/unifiedresources/registry.go` so matcher seeding, manual links, +and merge semantics stay unified-resource-owned instead of being rebuilt in +consumers. +Storage consumer projection is unified-resource-owned through +`internal/unifiedresources/storage_consumers.go`. When a provider publishes +source-native storage consumer metadata that cannot be derived from shared +Proxmox/PBS relationship indexes, refresh must preserve that source-owned +consumer count, consumer type list, and top-consumer summary on the +canonical storage resource unless a stronger shared consumer projection has +already populated those fields in the same refresh. +Operator-facing storage posture wording is part of that same ownership: +when multiple storage-risk reasons exist, shared posture helpers must prefer +the most decision-useful protection loss summary such as lost parity over a +generic disk-count aggregate, so resource drawers and incidents do not hide +the actual protection boundary behind a broader count phrase. 6. Add canonical governed name-resolution or policy-aware resource lookup behavior through `internal/unifiedresources/resolve.go` and `internal/unifiedresources/resolve_context.go` 8. Add or change discovery-support runtime under the resource drawer through `frontend-modern/src/components/Discovery/DiscoveryTab.tsx` for shell/presentation ownership, `frontend-modern/src/components/Discovery/useDiscoveryTabState.ts` for fetch, websocket-progress, manual-run triggering, and notes-mutation ownership, and `frontend-modern/src/components/Discovery/discoveryReadiness.ts` for the shared readiness verdict used by resource-drawer Discovery surfaces. Embedded drawers may expose the top-level run action through this shared Discovery tab, but they must still call the canonical discovery trigger state path instead of introducing drawer-local API mutations. +Drawer-level feature availability belongs to +`frontend-modern/src/components/Discovery/useDiscoveryFeatureAvailability.ts`. +It consumes the shared AI runtime settings store and fails closed until the +runtime explicitly reports `discovery_enabled=true`. Resource, guest, node, +and Docker host drawers must use that boundary for every Discovery tab, +readiness badge, analysis reveal, identified-service suggestion, and +passive discovery-record query. A disabled or unresolved feature must leave +no Discovery mention in drawer chrome or content and must not start a +drawer-local discovery read. +Resource drawer secondary sections, action history, discovery run summaries, +and other compact resource-detail cards may own their resource-specific +labels, rows, filters, and actions, but the repeated bordered compact frame +is a frontend-primitives boundary. `ResourceDetailDrawerOverviewTab.tsx`, +`ResourceActionHistory.tsx`, and `DiscoveryTab.tsx` must compose +`InfoCardFrame` for that shell instead of restoring local card-frame +classes. +Curated technical inventory follows the shared compact-row contract instead +of the secondary-card contract. Docker-host drawers must project system, +runtime, memory, storage, and telemetry facts through +`TechnicalDetailsSection` with canonical `DetailSection[]` data, while the +unified-resource drawer projects runtime, identity, container, tag, alias, +and address facts through the same `DetailSectionTable` responsive +presentation. Only genuinely large or interactive provider-support content stays +lazy behind `TechnicalDetailsDisclosure`; technical inventory must not +restore a local card mosaic or add a drawer-open fetch. 9. Keep dashboard and infrastructure freshness on the canonical unified-resource +ownership path. `frontend-modern/src/stores/websocket.ts`, +`frontend-modern/src/utils/resourceStateAdapters.ts`, and +`frontend-modern/src/hooks/useUnifiedResources.ts` together own the frontend +canonicalization boundary: REST may hydrate the initial snapshot and +unsupported filtered queries, but supported snapshot freshness must come +from websocket `state.resources` instead of layering confirmatory +route-local REST refetch loops over already-owned resource +updates. +Oversized WebSocket recovery is the transport exception: a complete +`/api/state` response may refresh display state while the connection remains +baseline-free, but resource deltas must not patch that independently built +REST snapshot. The store resumes delta application only after the same +connection delivers a complete WebSocket resource snapshot. +Browser WebSocket liveness tracking is part of that same store boundary: +valid inbound server messages, including heartbeat `ping`/`pong` traffic, +must refresh the browser-side activity timestamp so quiet periods between +resource snapshots do not cause avoidable reconnect churn. +That shared store/adapter/hook path must also preserve canonical row shape +across transport boundaries: thinner realtime `state.resources` payloads +must merge into the existing canonical resource snapshot instead of +downgrading richer REST-only infrastructure details such as disk I/O, source +metadata, or platform summary fields after first hydrate. For default +Source lists and their source-specific facets are the exception: a current +snapshot with canonical source evidence replaces stale source lists and +removes provider facets that no longer have matching source evidence, so +rows do not keep displaying a previous platform identity after websocket +refreshes. For default +`initialHydration: 'immediate'` consumers, that same path must not paint the +thinner websocket transport before the first canonical REST snapshot exists; +only explicit websocket-first consumers may render directly from the realtime +transport before canonical hydrate completes. Operator surfaces that must +preserve already-known infrastructure continuity after login, such as the +Infrastructure page, must use websocket-first hydration with stale-cache +REST revalidation after the first-paint settle window so the page can paint +from live state immediately without forcing a second resource-shape +transition while summary and table surfaces are still mounting. +Org-scope and enabled-state transitions in +`frontend-modern/src/hooks/useUnifiedResources.ts` must invalidate older +in-flight REST refreshes before publishing the new scoped cache entry, so a +stale request cannot set active-scope errors, clear the active request guard, +or replace the currently mounted Infrastructure/Workloads resource snapshot. +Canonical cluster membership in that shared path must come only from +explicit cluster identity such as Kubernetes context or platform cluster +labels; standalone resource names must never be repurposed as synthetic +`clusterId` values. 13. Keep operator-facing resource analysis vocabulary task-first on unified-resource +surfaces. `frontend-modern/src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx`, +`frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts`, +and `frontend-modern/src/components/Discovery/DiscoveryTab.tsx` may expose +provider identity or governed safe-summary posture when that context helps +an operator, but the rendered labels must stay product-neutral and use +`Analysis`, `Analysis Reasoning`, and `Safe Summary` rather than reviving +generic `AI` or `AI-Safe` branding inside the resource drawer or discovery +shell. 14. Keep the operator-facing unified resource table width-aware at the table +surface, not just at the browser viewport. `frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx` +must route its root ref through `frontend-modern/src/components/Infrastructure/useUnifiedResourceTableState.ts`, +and `frontend-modern/src/components/Infrastructure/unifiedResourceTableStateModel.ts` +owns the column-priority breakpoints for host and service infrastructure +rows. When the app shell leaves tablet-sized space during live resize, the +table hides lower-priority metadata first. At phone width, the state model +must remove the old 640-pixel floor, preserve identity at exactly 30 percent +of the table, and allocate the remaining width across the bounded +source-relevant health and activity columns. Both the document and table +shell must remain free of horizontal overflow; desktop and tablet stages +retain their existing complete column contracts. 15. Keep shared policy-posture framing on the unified-resource card owner. +`frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx` +may accept caller-owned subtitle or resource-count wording when Patrol or +another shared surface needs to explain how the same governed policy counts +should be read, but those framing lines must extend the shared card API +rather than spawning page-local policy summary shells. 16. Keep platform/runtime top-level route paths on the canonical resource-link +helper. `frontend-modern/src/routing/resourceLinks.ts` owns the +`STANDALONE_PATH`, `DOCKER_PATH`, `KUBERNETES_PATH`, `TRUENAS_PATH`, +`VMWARE_PATH`, `PATROL_PATH`, `PATROL_CONTROL_ANCHOR`, +`PATROL_CONTROL_PATH`, and `PATROL_CONTROL_STARTER_QUERY_PARAM` constants, +the route-backed Patrol control starter helpers, and the `buildStandalonePath`, +`buildDockerPath`, `buildKubernetesPath`, `buildTrueNASPath`, +`buildVmwarePath` builders. +Per-platform surfaces and tab specs must +derive every internal link from those builders so the canonical resource +URL vocabulary stays single-sourced; ad hoc string concatenation of +platform routes inside feature directories is not permitted. The canonical +Pulse Intelligence external-agent hash +`/settings/pulse-intelligence/assistant#external-agent-setup` and legacy +`/settings/security/api#external-agent-setup` / +`/settings/security/api#pulse-mcp-setup` compatibility hashes may live in +the shared route helper, but they are adjacent settings route state, not +unified-resource identity, platform scope, or drawer focus state. +The Patrol `patrolControlStarter=patrol_control` query is an adjacent +first-party Patrol control handoff flag, with legacy +`operationsLoopStarter` values accepted only as compatibility aliases, not +unified-resource filters or focus keys. Unified-resource consumers must not reuse those values for resource +identity, list filtering, contextual focus, storage state, recovery state, +or platform scoping. +The user-facing Machines surface's default resource route is the machines projection +(`/standalone/machines`); agentless endpoint rows use the +`/standalone/availability` projection and must not be collapsed into a +generic overview URL. +The frontend-primitives-owned Machines IA contract consumes the +unified-resource projection for Pulse-managed standalone agent rows and +agentless availability endpoint rows; this subsystem owns only the +membership rules for those projected rows. Agent membership must require +`resource.type === "agent"`, canonical Pulse-agent source evidence from +resource sources or source status, and no stronger provider-owner evidence +from Proxmox, VMware, TrueNAS, or Kubernetes. Source-less legacy snapshots +may fall back to a normalized `platformType === "agent"`, but +provider-owned nodes must not become machine-page members through +hostname, `agent` platform scope, or agent telemetry alone; those facts +surface as facets on the owning provider page. +`AgentsMachinesTable.tsx` may own row membership, resource-derived menu +eligibility, evidence-gated column relevance, and remove-agent semantics +for these projected rows. In particular, the GPU metric column and its +View choice are relevant only when at least one projected machine reports +finite GPU utilization; absent telemetry must not produce an empty default +column or a no-op column choice, while persisted visibility remains ready +for the column when evidence later appears. The +compact row action trigger chrome stays under the frontend-primitives +`ActionIconButton` boundary rather than becoming a unified-resource-local +button shell. +Machines list search and online-state narrowing are frontend route state, +not new unified-resource membership fields. `StandalonePageSurface.tsx` +owns the `STANDALONE_QUERY_PARAMS` query/status projection and one composite +reset, while `AgentsMachinesTable.tsx` consumes those controlled values so +saved links and bookmarks cannot diverge from the canonical projected row +set. Those query parameters must only narrow the already-owned agent +projection; they must not cause provider nodes or availability endpoints to +enter the Machines membership bucket. +`PROXMOX_BACKUPS_QUERY_PARAMS` in the same shared route-helper module is +storage/recovery-owned filter and workspace state for the Proxmox Backups +surface. Its query, view, node, type, source, posture, and day keys may +narrow already-correlated backup rows, but they do not add unified-resource +membership, change canonical workload identity, or turn recovery evidence +into a provider-resource projection. +The Proxmox backup workspace's chronological and coverage views are +canonical route state under `/proxmox/backups/date` and +`/proxmox/backups/coverage`. The shared route helper owns those path +builders; the backup surface must use the shared platform section-tab +primitive for navigation and may retain legacy query parsing only as +compatibility input. Those view paths describe backup evidence and must +not be treated as unified-resource membership or workload identity. +The default tab for each platform path must point at a sub-tab whose +canonical unified-resource projection actually populates, and visible +workflow subtabs must stay evidence-gated by the same canonical row or +signal source instead of advertising empty object browsers. The +canonical TrueNAS adapter (`internal/truenas/provider.go:: truenasRecordsFromSnapshot`) already emits the top-level TrueNAS - appliance as a unified `agent` row tagged with the `truenas` - platform, so TrueNAS defaults to `/truenas/overview` (the Systems - sub-tab); the embedded `StorageSurface` lives at `/truenas/storage`. - Any future platform that wants to default to a Systems / Hosts - overview must first have its canonical resource adapter project the - platform's top-level system as a unified resource so the builder - default still resolves to a populated table. +appliance as a unified `agent` row tagged with the `truenas` +platform, so TrueNAS defaults to `/truenas/overview` (the Systems +sub-tab); the embedded `StorageSurface` lives at `/truenas/storage`. +Any future platform that wants to default to a Systems / Hosts +overview must first have its canonical resource adapter project the +platform's top-level system as a unified resource so the builder +default still resolves to a populated table. 17. Platform table ordering is a two-layer contract. Each platform table's default order is owned by its page model's status-first compare @@ -1613,7 +1605,7 @@ served clones. Proof: `TestClonedResourcesPreservePlatformAdmission` and the row may highlight in place through the shared active-resource id; if it is off-screen, the page must offer an explicit `Jump to row` affordance rather than auto-scrolling or collapsing the table on hover. -12a. Keep infrastructure summary visibility as display preference, not a + 12a. Keep infrastructure summary visibility as display preference, not a unified-resource filter. Platform/runtime pages and shared infrastructure summary consumers may hide or restore chart sections through shared presentation controls, but those controls must not mutate resource @@ -2025,7 +2017,6 @@ served clones. Proof: `TestClonedResourcesPreservePlatformAdmission` and `internal/recovery/store/store_test.go` (`TestStore_OpenBackfillsLegacyUnresolvedProxmoxPBSGuestRows`). - ## Current State ### Agent libvirt domains use a provider-neutral VM facet @@ -2199,7 +2190,9 @@ rendering or frontend fallback code. `resource_operator_state.go` owns the operator-set per-resource intent schema. `ResourceOperatorState` carries five narrow operator-intent fields (`IntentionallyOffline`, `NeverAutoRemediate`, maintenance -window via `MaintenanceStartAt` / `MaintenanceEndAt` / `MaintenanceReason`, +window via one-shot `MaintenanceStartAt` / `MaintenanceEndAt` or a mutually +exclusive weekly `MaintenanceRecurrence`, shared `MaintenanceReason`, and +explicit `MaintenanceScope`, canonical `Criticality` hint of `high|medium|low|""`, and an explicit `AutoRemediationPolicy`) plus operator-attribution metadata (`Note`, `SetAt`, `SetBy`). The shape is @@ -2219,6 +2212,18 @@ is the explicit per-resource opt-out and always wins. Capability-owned capability is never eligible, low-risk eligible, or elevated eligible; it does not lower `MinimumApprovalLevel`. The first eligible vertical is Docker/Podman container `restart` at `low_risk`; unspecified capabilities normalize to never. +Recurring maintenance names the local weekdays on which occurrences start, +uses inclusive start and exclusive end minutes, may cross midnight, and is +evaluated in its stored IANA timezone so DST and server relocation cannot +silently move operator intent. Weekdays normalize to unique Monday-through- +Sunday order. `maintenanceScope` defaults to `resource`; the only inherited +value is `resource_and_descendants`, resolved against the canonical registry +parent chain with cycle protection. Concrete occurrence start/end boundaries +are derived by the same model for Alerts and post-window verification. SQLite +persists recurrence JSON and scope in additive columns while legacy one-shot +rows retain their existing meaning. Scheduling, updating, changing scope, or +clearing either form remains an atomic operator-state plus resource-timeline +lifecycle write. `NormalizeResourceOperatorState` trims whitespace, de-duplicates capability names, guarantees a non-nil empty capability list, and lower-cases the criticality value before persistence. SQLite reads must apply that diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 42b00795b..f3c94011b 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,30 +1,33 @@ { "version": 1, - "base_sha": "d48e70da152525471c1d1ee3364c92c76b4d06af", - "verified_at": "2026-08-27T17:00:26Z", + "base_sha": "ce555ee09d7cef30a2d784c18c629272aef9ab22", + "verified_at": "2026-08-27T17:11:50Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Infrastructure/ResourceDetailSummary.tsx", - "frontend-modern/src/components/shared/DetailSectionTable.tsx", - "frontend-modern/src/components/shared/detailSectionModel.ts" + "frontend-modern/src/api/resourceOperatorState.ts", + "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx", + "frontend-modern/src/features/alerts/ResourceMonitoringPolicyAction.tsx" ], "content_sha256": { - "frontend-modern/src/components/Infrastructure/ResourceDetailSummary.tsx": "438dd4e834e576911bcf06d8849f832bb59e9349ff6476a3b437b356ecdf3d1a", - "frontend-modern/src/components/shared/DetailSectionTable.tsx": "5b2592463049e3b1740fa8c63cabb09529a83bd1cd64509c57b473d3b05dc35a", - "frontend-modern/src/components/shared/detailSectionModel.ts": "da27583092cad09d50ea03c4308f7841bd2475e8c6539527142da06a78d9722f" + "frontend-modern/src/api/resourceOperatorState.ts": "edbbb0253d63fba3f8c893edb4c911948883c88ba1ee61ecdac453853d370de4", + "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx": "9b45eb2a826c6db7fb7a47ec3dcc341c1ed47109228d83ed085788e647c69012", + "frontend-modern/src/features/alerts/ResourceMonitoringPolicyAction.tsx": "fc2e4d010d80419e9532c49cddd9df954672c2d4a3b5de9d3336fd483562a4ad" }, - "routes": ["/proxmox/overview"], + "routes": ["/alerts/overview", "/proxmox/overview"], "viewports": [ - { "width": 1920, "height": 800 }, + { "width": 1280, "height": 800 }, { "width": 390, "height": 844 } ], "states": [ - "expanded backup-vault backup-server resource drawer with Overview selected", - "runtime context and identity sections with rich alias values" + "active Alerts overview with the per-alert Monitoring policy action", + "newly discovered resource with no explicit operator-state record", + "resource Manage drawer with the maintenance scheduler open in recurring mode", + "recurring schedule with weekday, Europe/London timezone, and descendant scope selected" ], "interactions": [ - "expanded the first backup-vault row and confirmed two aligned equal-height desktop cards with a local 7rem label column", - "confirmed the same sections retain compact native table rows at 390x844 with zero document horizontal overflow", - "reloaded the final implementation and confirmed no new browser console errors" + "opened an alert Monitoring action and confirmed the canonical policy choices render without changing saved state", + "opened a resource with no saved operator state and confirmed the lookup completes without a failed response or console error", + "opened Manage, switched the maintenance scheduler from One time to Recurring, and exercised weekday, timezone, and descendant-scope controls without saving", + "verified the full scheduler at 1280x800 and 390x844 with no horizontal overflow, interaction console errors, or failed interaction responses" ] } diff --git a/frontend-modern/src/api/__tests__/resourceOperatorState.test.ts b/frontend-modern/src/api/__tests__/resourceOperatorState.test.ts index d96814a5d..9f44f74ca 100644 --- a/frontend-modern/src/api/__tests__/resourceOperatorState.test.ts +++ b/frontend-modern/src/api/__tests__/resourceOperatorState.test.ts @@ -33,12 +33,18 @@ describe('resourceOperatorState api', () => { expect(apiFetchJSONMock).toHaveBeenCalledWith( // colons are reserved in URL paths and must be percent-encoded // before the canonical id reaches the server router. - '/api/resources/instance%3Anode%3A101/operator-state', + '/api/resources/instance%3Anode%3A101/operator-state?view=lookup', { cache: 'no-store' }, ); }); - it('returns null when the server reports operator_state_not_set as 404', async () => { + it('returns null from the successful lookup envelope when no state is configured', async () => { + apiFetchJSONMock.mockResolvedValueOnce({ configured: false }); + + await expect(getResourceOperatorState('vm:101')).resolves.toBeNull(); + }); + + it('returns null when an older server reports operator_state_not_set as 404', async () => { apiFetchJSONMock.mockRejectedValueOnce(Object.assign(new Error('Not found'), { status: 404 })); await expect(getResourceOperatorState('vm:101')).resolves.toBeNull(); diff --git a/frontend-modern/src/api/resourceOperatorState.ts b/frontend-modern/src/api/resourceOperatorState.ts index 1b0f641b0..2643fd217 100644 --- a/frontend-modern/src/api/resourceOperatorState.ts +++ b/frontend-modern/src/api/resourceOperatorState.ts @@ -10,6 +10,14 @@ import { apiFetchJSON } from '@/utils/apiClient'; export type ResourceCriticality = 'high' | 'medium' | 'low' | ''; export type ResourceMonitoringMode = 'normal' | 'expected_offline' | 'muted'; export type ResourceLifecycleState = 'active' | 'retired'; +export type MaintenanceScope = 'resource' | 'resource_and_descendants'; + +export interface RecurringMaintenanceWindow { + timezone: string; + weekdays: string[]; + startMinute: number; + endMinute: number; +} export interface AutoRemediationWindow { timezone: string; @@ -57,7 +65,12 @@ export interface ResourceOperatorState { */ maintenanceStartAt?: string; maintenanceEndAt?: string; + maintenanceRecurrence?: RecurringMaintenanceWindow; + maintenanceScope?: MaintenanceScope; maintenanceReason?: string; + maintenanceWindowActive?: boolean; + maintenanceActiveStartAt?: string; + maintenanceActiveEndAt?: string; /** * Optional operator hint that affects finding sort order. One of * `'high' | 'medium' | 'low' | ''` (empty = default). @@ -68,6 +81,11 @@ export interface ResourceOperatorState { setBy?: string; } +interface ResourceOperatorStateLookup { + configured: boolean; + state?: ResourceOperatorState; +} + /** * The PUT body shape — same as the read model but with attribution * stripped because the server populates `setAt` and `setBy` from the @@ -75,7 +93,12 @@ export interface ResourceOperatorState { */ export type ResourceOperatorStateInput = Omit< ResourceOperatorState, - 'canonicalId' | 'setAt' | 'setBy' + | 'canonicalId' + | 'setAt' + | 'setBy' + | 'maintenanceWindowActive' + | 'maintenanceActiveStartAt' + | 'maintenanceActiveEndAt' >; const normalizeResourceOperatorState = (state: ResourceOperatorState): ResourceOperatorState => ({ @@ -83,26 +106,33 @@ const normalizeResourceOperatorState = (state: ResourceOperatorState): ResourceO monitoringMode: state.monitoringMode || (state.intentionallyOffline ? 'expected_offline' : 'normal'), lifecycleState: state.lifecycleState || 'active', + maintenanceScope: state.maintenanceScope || 'resource', }); /** - * Read the operator-set state for a resource. Resolves to null when - * the server returns 404 (no entry recorded — the default no-state - * posture). Throws on other errors. + * Read the operator-set state for a resource. The lookup view represents an + * unset record as a successful envelope so opening a newly discovered + * resource does not generate a routine 404 in the browser. The 404 fallback + * keeps the frontend compatible with an older server during a rolling update. */ export async function getResourceOperatorState( resourceId: string, ): Promise { try { - const state = await apiFetchJSON( - `/api/resources/${encodeURIComponent(resourceId)}/operator-state`, + const result = await apiFetchJSON( + `/api/resources/${encodeURIComponent(resourceId)}/operator-state?view=lookup`, { cache: 'no-store' }, ); - return normalizeResourceOperatorState(state); + if ('configured' in result) { + if (!result.configured || !result.state) return null; + return normalizeResourceOperatorState(result.state); + } + // Rolling-update compatibility: a previous server returns the persisted + // state directly when it exists. + return normalizeResourceOperatorState(result); } catch (err) { - // The 404 response shape is `{ error: 'operator_state_not_set', ... }`. - // Translating into null lets the caller treat "no state" as a clean - // default rather than a thrown error. + // Previous servers express no saved state as 404 + // `{ error: 'operator_state_not_set', ... }`. if ( err && typeof err === 'object' && diff --git a/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx b/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx index d5da2af6f..e75654581 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx @@ -115,10 +115,38 @@ export const ResourceOperatorStateSection: Component('once'); const [scheduleStart, setScheduleStart] = createSignal(''); const [scheduleEnd, setScheduleEnd] = createSignal(''); + const [scheduleWeekdays, setScheduleWeekdays] = createSignal([ + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday', + ]); + const [scheduleRecurringStart, setScheduleRecurringStart] = createSignal('02:00'); + const [scheduleRecurringEnd, setScheduleRecurringEnd] = createSignal('03:00'); + const [scheduleTimezone, setScheduleTimezone] = createSignal( + Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'UTC', + ); + const [scheduleScope, setScheduleScope] = createSignal<'resource' | 'resource_and_descendants'>( + 'resource', + ); const [scheduleReason, setScheduleReason] = createSignal(''); + const maintenanceWeekdays = [ + ['monday', 'Mon'], + ['tuesday', 'Tue'], + ['wednesday', 'Wed'], + ['thursday', 'Thu'], + ['friday', 'Fri'], + ['saturday', 'Sat'], + ['sunday', 'Sun'], + ] as const; + // Hydrate edit state from persisted record on first load and on resource change. createEffect(() => { const current = persisted(); @@ -225,6 +253,8 @@ export const ResourceOperatorStateSection: Component { const current = persisted(); + if (current?.maintenanceWindowActive && current.maintenanceActiveEndAt) return current; if (!current?.maintenanceStartAt || !current?.maintenanceEndAt) return null; const now = Date.now(); const start = Date.parse(current.maintenanceStartAt); @@ -333,7 +364,10 @@ export const ResourceOperatorStateSection: Component - Boolean(activeMaintenanceWindow() || scheduledMaintenanceWindow()), + Boolean( + persisted()?.maintenanceRecurrence || + (persisted()?.maintenanceStartAt && persisted()?.maintenanceEndAt), + ), ); // Datetime-local input format is "YYYY-MM-DDTHH:mm" in the browser's @@ -357,13 +391,23 @@ export const ResourceOperatorStateSection: Component { + setScheduleWeekdays((current) => + current.includes(weekday) + ? current.filter((candidate) => candidate !== weekday) + : [...current, weekday], + ); + }; + const scheduleValidationError = createMemo(() => { + if (scheduleKind() === 'recurring') { + if (scheduleWeekdays().length === 0) return 'Select at least one day.'; + if (!scheduleTimezone().trim()) return 'Timezone is required.'; + if (scheduleRecurringStart() === scheduleRecurringEnd()) { + return 'Recurring start and end times must differ.'; + } + return null; + } const start = parseLocalFromInput(scheduleStart()); const end = parseLocalFromInput(scheduleEnd()); if (!start || !end) return 'Both start and end are required.'; @@ -388,11 +448,11 @@ export const ResourceOperatorStateSection: Component { const start = parseLocalFromInput(scheduleStart()); const end = parseLocalFromInput(scheduleEnd()); - if (!start || !end) { + if (scheduleKind() === 'once' && (!start || !end)) { notificationStore.error('Both start and end are required.'); return; } - if (end.getTime() <= start.getTime()) { + if (scheduleKind() === 'once' && end!.getTime() <= start!.getTime()) { notificationStore.error('Maintenance end must be strictly after start.'); return; } @@ -418,8 +478,18 @@ export const ResourceOperatorStateSection: Component
Maintenance window active. Findings raised on this - resource are auto-acknowledged until{' '} - {formatRelativeTime(activeMaintenanceWindow()!.maintenanceEndAt!, { compact: true })}. + resource + + {' '} + and its descendants + {' '} + are auto-acknowledged until{' '} + {formatRelativeTime( + activeMaintenanceWindow()!.maintenanceActiveEndAt ?? + activeMaintenanceWindow()!.maintenanceEndAt!, + { compact: true }, + )} + . Reason: {activeMaintenanceWindow()!.maintenanceReason} @@ -528,6 +610,26 @@ export const ResourceOperatorStateSection: Component + +
+ Recurring maintenance configured.{' '} + {persisted()! + .maintenanceRecurrence!.weekdays.map((weekday) => weekday.slice(0, 3)) + .join(', ')}{' '} + from {minuteToTime(persisted()!.maintenanceRecurrence!.startMinute)} to{' '} + {minuteToTime(persisted()!.maintenanceRecurrence!.endMinute)}{' '} + {persisted()!.maintenanceRecurrence!.timezone}. + + + This resource and all canonical descendants are covered. + + + + Reason: {persisted()!.maintenanceReason} + +
+
+
Schedule maintenance window
-
- - +
+ +
-
- Quick presets: - - - -
+ +
+ + +
+
+ Quick presets: + + {(hours) => ( + + )} + +
+
+ + +
+
+ Days the window starts +
+ + {([value, label]) => ( + + )} + +
+
+
+ + + +
+

+ End times earlier than start times continue into the following day. +

+
+
+ + + setScheduleScope(event.currentTarget.value as 'resource' | 'resource_and_descendants') + } + help="Descendant scope follows Pulse's canonical inventory hierarchy and covers resources added beneath this one later." + helpClass="text-[11px] leading-tight" + > + + +