diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 820a0bef5..064668a55 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -1169,10 +1169,15 @@ not a replacement status card, CTA band, or page-local nested card. `PlatformTableShell` marks every owned table with the shared `platform-table` class; direct Workloads and Storage consumers and nested provider detail tables must apply that same - marker so the phone contract cannot vary by rendering path. Text-only - operational rows must use `getPlatformTableRowClass` so the absence of - buttons, badges, or progress bars does not collapse them below the canonical - compact scanning rhythm. Mobile truncation may rely on a full-value row + marker so the phone contract cannot vary by rendering path. The marker owns + a canonical 32-pixel summary-row rhythm across every provider and excludes + inline detail rows, which remain content-sized. Summary cells must render + one line only: secondary identity, raw provider labels, and descriptive + context belong in an existing operational column, a supplemental tooltip, + or its inline detail drawer rather than a stacked subtitle. Text-only + operational rows may use `getPlatformTableRowClass` to declare that same + shared rhythm explicitly; providers must not introduce local row-height + exceptions. Mobile truncation may rely on a full-value row detail only when that row actually has a keyboard- and touch-operable disclosure; a non-expandable identity cell must otherwise expose its complete value without requiring hover. Default data rows stay single-line @@ -3925,7 +3930,12 @@ label and destination intent; they must not own CTA button chrome. Platform table frames are one of those registry-backed templates. `frontend-modern/src/features/platformPage/sharedPlatformPage.tsx` owns `PlatformTableShell`, including the canonical table card, header row, and body -divide styling. It also owns the final responsive table-class composition, so +divide styling, plus the single-line 32-pixel summary-row rhythm inherited by +Proxmox, Docker / Podman, Kubernetes, TrueNAS, vSphere, Standalone, Workloads, +and direct Storage consumers. Inline detail rows remain content-sized. +Secondary context must move to an existing column, a supplemental tooltip, or +the inline detail drawer instead of stacking a second visual line. The shell +also owns the final responsive table-class composition, so feature tables may supply breakpoint floors but cannot accidentally override the shared phone-width floor with `min-w-full`. Platform table frames now have no local-frame exceptions in `shared-template-registry.json`: new and existing diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index c94ff2a23..3899d854d 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -90,7 +90,6 @@ it: that connection is invisible to the mapping rather than absent from the source, and resolution stays inconclusive for the PBS instance until every candidate connection has been observed. - Physical-disk to node association is evidence-directional. A disk reported by a host agent carries no Proxmox scope, so its instance is empty and the matching node name is the only evidence available; that match must stand. A disk that @@ -197,11 +196,11 @@ a recovery-provider read helper or compatibility alias. 2. `frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx` shared with `unified-resources`: Proxmox workload coverage rows are both a storage/recovery protection-posture surface and a unified-resource identity consumer boundary. 3. `frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx` shared with `unified-resources`: Proxmox recoverable workload table rows are both a storage/recovery coverage surface and a unified-resource platform-table consumer boundary. 4. `internal/api/configapi/setup_script_render.go` shared with `agent-lifecycle`, `api-contracts`: the generated Proxmox setup-script is a shared boundary across agent lifecycle (forced-command keys, install/uninstall edits), API contracts (rendered token shape and encoded rerun URL), and storage/recovery (backup visibility grants, Pulse-managed temperature SSH keys, and SMART disk-temperature collection). - The user-chosen connection name carried by assisted setup travels on the - server-side setup token, not through the rendered script or its - registration payload, so honoring it changes neither the backup - visibility grants the script applies nor the storage-side identity that - backup and recovery attribution key on (hostname and candidate hosts). + The user-chosen connection name carried by assisted setup travels on the + server-side setup token, not through the rendered script or its + registration payload, so honoring it changes neither the backup + visibility grants the script applies nor the storage-side identity that + backup and recovery attribution key on (hostname and candidate hosts). 5. `internal/proxmoxidentity/backup_identity.go` shared with `alerts`, `monitoring`: Proxmox PBS backup subject identity is a shared runtime boundary for monitoring backup freshness, backup-age alert attribution, and recovery-point guest mapping. Storage resource projections may show alert indicators from the canonical @@ -465,6 +464,14 @@ Proxmox backup source/state chips are also a frontend-primitives boundary: badge tones, `proxmoxBackupsTableShared.tsx` owns the backup table helper composition, and visible source/state chips must render through `MetadataBadge` instead of restoring local rounded-sm xs badge spans. +Proxmox backup-health and recoverable-artifact summary rows follow the same +platform-table density contract as every other provider surface. Storage and +recovery owns which backup, datastore, workload-type, and VMID facts matter to +an operator, while frontend-primitives owns the canonical 32px single-line +summary row. Compact layouts must not stack those secondary facts beneath the +resource name and make the row taller: keep essential identity inline when no +detail disclosure exists, and otherwise preserve supporting context in its +existing column, the row's accessible title, or the expanded detail content. The Proxmox backup view selector follows the same ownership split: storage/recovery owns the chronological-versus-coverage view values and backup semantics, while the visible segmented selector shell must compose the @@ -863,7 +870,7 @@ outside the storage/recovery contract and must not become restore material, recovery scope, or a storage/recovery-owned secret source. 1. Add or change recovery-point persistence, rollups, or series derivation through `internal/recovery/` -4. Route transport changes for storage and recovery endpoints through `internal/api/` and the owning `api-contracts` proof routes +2. Route transport changes for storage and recovery endpoints through `internal/api/` and the owning `api-contracts` proof routes Commercial migration startup behavior in `internal/api/licensing_handlers.go` and `internal/api/licensing_bridge.go` remains adjacent cloud-paid/API state. Synthetic mock-license suppression @@ -1093,7 +1100,7 @@ recovery scope, or a storage/recovery-owned secret source. both directions: they may show their existing gated-capability affordances when the policy exposes them, and they must not add edition- or license-sniffing of their own to re-suppress or re-expose paid prompts. -5. Route canonical storage/recovery resource selection through `frontend-modern/src/hooks/useUnifiedResources.ts` and the owning `unified-resources` contract +3. Route canonical storage/recovery resource selection through `frontend-modern/src/hooks/useUnifiedResources.ts` and the owning `unified-resources` contract That shared hook now also projects resource `clusterId` through the shared cluster-name helper, so storage and recovery links keep the same cluster-context label as other unified-resource consumers instead of rebuilding a local fallback chain. That shared hook plus the adjacent websocket/store adapter path must keep realtime transport merges canonical for storage/recovery consumers too: @@ -1142,7 +1149,7 @@ recovery scope, or a storage/recovery-owned secret source. are visible must now pass an explicit enabled gate into that shared hook and any adjacent recovery-rollup query, so hidden workload-route selectors do not hydrate storage/recovery transport on the protected hot path. -6. Preserve API-owned node identity continuity in shared `internal/api/` helpers so storage and recovery transport attachments do not fork by hostname-versus-IP drift across the same runtime. +4. Preserve API-owned node identity continuity in shared `internal/api/` helpers so storage and recovery transport attachments do not fork by hostname-versus-IP drift across the same runtime. That same adjacent `internal/api/` boundary also owns canonical host-alias propagation on grouped-system and attached-connection payloads. `internal/api/connections_types.go`, @@ -1201,7 +1208,7 @@ recovery scope, or a storage/recovery-owned secret source. they must not treat that response as a source for owner identity because the stable hosted owner principal is server-side org metadata resolved later through magic-link verification. -7. Preserve fail-closed API assignment and lookup behavior in shared `internal/api/` helpers so storage and recovery surfaces do not inherit orphaned profile or resource references from unrelated transport mutations. +5. Preserve fail-closed API assignment and lookup behavior in shared `internal/api/` helpers so storage and recovery surfaces do not inherit orphaned profile or resource references from unrelated transport mutations. Preserve fail-closed proxy-auth administrator evaluation in those same shared helpers as an adjacent security/API boundary: storage and recovery surfaces may consume an already-authorized admin request, but they must not @@ -1213,12 +1220,12 @@ recovery scope, or a storage/recovery-owned secret source. visibility, restore authority, protected-system ownership, and recovery-local audit attribution must continue to use backend-authenticated stable principals and storage/recovery resource identities. -8. Preserve canonical configured public endpoint selection in shared `internal/api/` helpers so recovery and storage links do not inherit loopback-local scheme drift from admin-originated setup/install flows. -9. Preserve trailing-slash normalization in those shared install-command helpers so recovery-adjacent transport and link surfaces do not inherit double-slash installer paths or slash-suffixed public endpoint drift from canonical backend install payloads. -10. Preserve canonical /api/auto-register token-action truth in shared `internal/api/` helpers so adjacent setup and recovery-adjacent transport flows stay on caller-supplied credential completion instead of reviving deleted alternate completion modes. -11. Preserve the canonical setup-script `source="script"` marker through those same shared auto-register helpers, and reject non-canonical source labels there, so later canonical reruns can keep treating script-confirmed tokens differently from agent-created tokens without reviving arbitrary caller-label compatibility. -12. Preserve the canonical auto-register node-type boundary in those same shared helpers so only supported `pve` and `pbs` registrations can complete, and unsupported runtime labels cannot bleed fake node identities into adjacent transport or recovery-adjacent state. -13. Preserve the canonical auto-register token-identity boundary in those same shared helpers so only Pulse-managed `pulse-monitor@{pve|pbs}!pulse-` token IDs matching the requested node type can complete, and arbitrary, cross-type, or non-Pulse-managed token identities cannot bleed into adjacent transport or recovery-adjacent state. +6. Preserve canonical configured public endpoint selection in shared `internal/api/` helpers so recovery and storage links do not inherit loopback-local scheme drift from admin-originated setup/install flows. +7. Preserve trailing-slash normalization in those shared install-command helpers so recovery-adjacent transport and link surfaces do not inherit double-slash installer paths or slash-suffixed public endpoint drift from canonical backend install payloads. +8. Preserve canonical /api/auto-register token-action truth in shared `internal/api/` helpers so adjacent setup and recovery-adjacent transport flows stay on caller-supplied credential completion instead of reviving deleted alternate completion modes. +9. Preserve the canonical setup-script `source="script"` marker through those same shared auto-register helpers, and reject non-canonical source labels there, so later canonical reruns can keep treating script-confirmed tokens differently from agent-created tokens without reviving arbitrary caller-label compatibility. +10. Preserve the canonical auto-register node-type boundary in those same shared helpers so only supported `pve` and `pbs` registrations can complete, and unsupported runtime labels cannot bleed fake node identities into adjacent transport or recovery-adjacent state. +11. Preserve the canonical auto-register token-identity boundary in those same shared helpers so only Pulse-managed `pulse-monitor@{pve|pbs}!pulse-` token IDs matching the requested node type can complete, and arbitrary, cross-type, or non-Pulse-managed token identities cannot bleed into adjacent transport or recovery-adjacent state. Preserve canonical auto-register event intent in those same shared helpers: only first-time node creation may emit the toast-bearing `node_auto_registered` WebSocket event, while idempotent existing-node @@ -1238,7 +1245,7 @@ recovery scope, or a storage/recovery-owned secret source. same discovery-refresh plus node-deleted websocket side effects as a manual delete so adjacent recovery/storage surfaces do not retain stale provider context after Pulse credentials have been removed from the host. -14. Preserve canonical /api/auto-register DHCP continuity in those shared helpers so a PVE or PBS node that reruns registration from a new IP with the same canonical node name and deterministic Pulse-managed token identity updates in place instead of duplicating the inventory record. +12. Preserve canonical /api/auto-register DHCP continuity in those shared helpers so a PVE or PBS node that reruns registration from a new IP with the same canonical node name and deterministic Pulse-managed token identity updates in place instead of duplicating the inventory record. Known contradictory TLS fingerprints must veto inferred resolved-address, changed-address name/token, and cluster-member continuity matches so separate protected sources are not overwritten when sites reuse node @@ -1260,18 +1267,18 @@ recovery scope, or a storage/recovery-owned secret source. transport flows must not reintroduce local marker trust or token rotation when the canonical auto-register helper can verify whether Pulse still has a matching node. -15. Preserve the governed root-or-sudo Unix wrapper in shared backend install-command helpers so storage- and recovery-adjacent transport surfaces do not inherit a stale raw `| bash -s --` install payload shape from the canonical agent-install-command API and hosted Proxmox install responses. -16. Preserve optional-auth tokenless behavior in those same shared backend install-command helpers so adjacent transport surfaces do not implicitly persist API tokens and flip auth-configured state when an operator only requested a Proxmox install command on a token-optional Pulse instance. -17. Preserve backend-owned Pulse Mobile relay runtime credential minting in those same shared `internal/api/` auth/security helpers so storage- and recovery-adjacent transport surfaces do not inherit browser-authored wildcard token bundles when they depend on the canonical security helper layer. -18. Preserve the dedicated backend-owned `relay:mobile:access` capability and its governed backward-compatible route inventory plus the shared helper call sites around it, so storage- and recovery-adjacent transport surfaces do not treat the mobile relay credential as a general AI scope bundle. +13. Preserve the governed root-or-sudo Unix wrapper in shared backend install-command helpers so storage- and recovery-adjacent transport surfaces do not inherit a stale raw `| bash -s --` install payload shape from the canonical agent-install-command API and hosted Proxmox install responses. +14. Preserve optional-auth tokenless behavior in those same shared backend install-command helpers so adjacent transport surfaces do not implicitly persist API tokens and flip auth-configured state when an operator only requested a Proxmox install command on a token-optional Pulse instance. +15. Preserve backend-owned Pulse Mobile relay runtime credential minting in those same shared `internal/api/` auth/security helpers so storage- and recovery-adjacent transport surfaces do not inherit browser-authored wildcard token bundles when they depend on the canonical security helper layer. +16. Preserve the dedicated backend-owned `relay:mobile:access` capability and its governed backward-compatible route inventory plus the shared helper call sites around it, so storage- and recovery-adjacent transport surfaces do not treat the mobile relay credential as a general AI scope bundle. That same shared `internal/api/` machine boundary also owns hosted entitlement refresh targeting: when storage- or recovery-adjacent hosted routes execute under a tenant org without org-local billing state, the refresh path must repair the instance-level `default` lease and evaluator instead of rewriting the empty tenant org, so AI-guided recovery and hosted diagnostics do not collapse into false free-tier behavior. -19. Preserve shipped local security-doc guidance in shared `internal/api/` config/setup helpers so storage- and recovery-adjacent transport surfaces do not reintroduce GitHub `main` security links when the running build already serves its own local security documentation route. -20. Keep shared `internal/api/` Patrol transport and alert-trigger edits feature-isolated: Patrol-specific recency fields, callback fan-out, or alert-bridge wiring changes must not leak into recovery queries, storage links, or recovery-adjacent install/setup flows unless this contract changes in the same slice. +17. Preserve shipped local security-doc guidance in shared `internal/api/` config/setup helpers so storage- and recovery-adjacent transport surfaces do not reintroduce GitHub `main` security links when the running build already serves its own local security documentation route. +18. Keep shared `internal/api/` Patrol transport and alert-trigger edits feature-isolated: Patrol-specific recency fields, callback fan-out, or alert-bridge wiring changes must not leak into recovery queries, storage links, or recovery-adjacent install/setup flows unless this contract changes in the same slice. Retained Patrol objective routes follow the same adjacency rule. A brief, canonical resource scope, observer lifecycle, or derived coverage state is AI/runtime plus API-contract context; storage and recovery consumers must not @@ -1477,47 +1484,47 @@ recovery scope, or a storage/recovery-owned secret source. normalize the message, but they must not reinterpret it as a storage install prerequisite, provider-connection error, or recovery-capability verdict. -21. Keep provider-backed recovery onboarding on the adjacent platform-connections contract. When `internal/api/` grows or changes TrueNAS connection CRUD, masked-secret preservation, saved-connection retest routes, edit-form saved-test payload overlays, or similar provider setup flows, storage and recovery may consume the resulting recovery points but must not absorb that connection-management ownership into storage/recovery-local handlers or page flows. That same adjacency also covers per-surface scope as it flows through the unified connections aggregator: when `internal/api/connections_aggregator.go` projects TrueNAS `MonitorDatasets`/`MonitorPools`/`MonitorReplication` flags into the aggregator's `scope` map, storage and recovery may observe that projection to explain dataset/pool/replication coverage to operators but must not reinterpret those flags as a recovery freshness verdict, restore-capability gate, or storage-local scope registry. - That same adjacent platform-connections boundary now also owns - source-oriented `systems[]` grouping on `/api/connections`. Storage and - recovery may observe grouped source composition to explain whether a - platform row is collecting additional host telemetry through Pulse Agent, - but they must not reinterpret attached agents as separate protected - systems, duplicate recovery inventory rows, or a storage-local ownership - model. The same shared `/api/connections` contract also owns compact - `agentIdentity` facts for agent-backed rows; storage and recovery may read - that metadata, including host-profile ids such as `unraid`, when they need - to label a represented host, but they must not rebuild OS/endpoint identity - from recovery inventory or alias heuristics, or reinterpret an agent - host-profile id as a storage provider platform. - When that grouped platform row is a Proxmox cluster, storage and - recovery must also treat the backend-authored cluster moniker as the - canonical row identity instead of re-expanding cluster-member agents into - sibling host rows or per-node storage owners. If the grouped row carries - backend-authored cluster member nodes, adjacent storage/recovery surfaces - may use that composition for explanatory UI only; they must not promote - those child nodes into a second top-level grouped-system taxonomy or infer - per-node storage ownership from the settings payload. The same shared - `/api/connections` payload also owns any agent-version/update facts and - fleet-governance posture carried alongside those grouped rows; adjacent - storage or recovery surfaces may reuse that signal for operator context, - but must not fork their own version-comparison semantics, desired/applied - config-drift classifier, rollout-state classifier, credential-health - classifier, command-policy vocabulary, or another agent lifecycle - vocabulary. If `/api/connections` reports agent config drift as pending or - unknown because no trustworthy applied fingerprint exists, storage and - recovery must preserve that uncertainty instead of translating it into a - storage-local current/drifted verdict. If `/api/connections` reports - `configDrift: not-applicable` with a current applied rollout because no - managed host-agent config override is assigned, storage and recovery must - preserve that no-rollout state instead of translating it into a pending - storage/recovery problem. - When `/api/connections` attaches an exact-match host agent to a blocked - Proxmox API source without fresh node inventory, storage and recovery must - treat that as one represented source with host telemetry, not as a second - protected system or a storage-local duplicate host. -22. Keep backend-native platform actions on the adjacent AI/runtime and platform contracts. When `internal/api/` wires native TrueNAS app control for Assistant, storage and recovery may consume the refreshed recovery points afterward, but they must not grow a parallel recovery-local action transport or action-specific payload shape. -23. Keep backend-native platform diagnostics on the adjacent AI/runtime and platform contracts. When `internal/api/` wires native TrueNAS app log reads for Assistant, storage and recovery may use those diagnostics during investigation, but they must not grow a parallel recovery-local log transport or diagnostic payload shape. +19. Keep provider-backed recovery onboarding on the adjacent platform-connections contract. When `internal/api/` grows or changes TrueNAS connection CRUD, masked-secret preservation, saved-connection retest routes, edit-form saved-test payload overlays, or similar provider setup flows, storage and recovery may consume the resulting recovery points but must not absorb that connection-management ownership into storage/recovery-local handlers or page flows. That same adjacency also covers per-surface scope as it flows through the unified connections aggregator: when `internal/api/connections_aggregator.go` projects TrueNAS `MonitorDatasets`/`MonitorPools`/`MonitorReplication` flags into the aggregator's `scope` map, storage and recovery may observe that projection to explain dataset/pool/replication coverage to operators but must not reinterpret those flags as a recovery freshness verdict, restore-capability gate, or storage-local scope registry. + That same adjacent platform-connections boundary now also owns + source-oriented `systems[]` grouping on `/api/connections`. Storage and + recovery may observe grouped source composition to explain whether a + platform row is collecting additional host telemetry through Pulse Agent, + but they must not reinterpret attached agents as separate protected + systems, duplicate recovery inventory rows, or a storage-local ownership + model. The same shared `/api/connections` contract also owns compact + `agentIdentity` facts for agent-backed rows; storage and recovery may read + that metadata, including host-profile ids such as `unraid`, when they need + to label a represented host, but they must not rebuild OS/endpoint identity + from recovery inventory or alias heuristics, or reinterpret an agent + host-profile id as a storage provider platform. + When that grouped platform row is a Proxmox cluster, storage and + recovery must also treat the backend-authored cluster moniker as the + canonical row identity instead of re-expanding cluster-member agents into + sibling host rows or per-node storage owners. If the grouped row carries + backend-authored cluster member nodes, adjacent storage/recovery surfaces + may use that composition for explanatory UI only; they must not promote + those child nodes into a second top-level grouped-system taxonomy or infer + per-node storage ownership from the settings payload. The same shared + `/api/connections` payload also owns any agent-version/update facts and + fleet-governance posture carried alongside those grouped rows; adjacent + storage or recovery surfaces may reuse that signal for operator context, + but must not fork their own version-comparison semantics, desired/applied + config-drift classifier, rollout-state classifier, credential-health + classifier, command-policy vocabulary, or another agent lifecycle + vocabulary. If `/api/connections` reports agent config drift as pending or + unknown because no trustworthy applied fingerprint exists, storage and + recovery must preserve that uncertainty instead of translating it into a + storage-local current/drifted verdict. If `/api/connections` reports + `configDrift: not-applicable` with a current applied rollout because no + managed host-agent config override is assigned, storage and recovery must + preserve that no-rollout state instead of translating it into a pending + storage/recovery problem. + When `/api/connections` attaches an exact-match host agent to a blocked + Proxmox API source without fresh node inventory, storage and recovery must + treat that as one represented source with host telemetry, not as a second + protected system or a storage-local duplicate host. +20. Keep backend-native platform actions on the adjacent AI/runtime and platform contracts. When `internal/api/` wires native TrueNAS app control for Assistant, storage and recovery may consume the refreshed recovery points afterward, but they must not grow a parallel recovery-local action transport or action-specific payload shape. +21. Keep backend-native platform diagnostics on the adjacent AI/runtime and platform contracts. When `internal/api/` wires native TrueNAS app log reads for Assistant, storage and recovery may use those diagnostics during investigation, but they must not grow a parallel recovery-local log transport or diagnostic payload shape. The same adjacent-boundary rule applies to `GET /api/agents/diagnostics`: storage and recovery may read Agent Fleet Doctor evidence as operational context for stale agents, version drift, profile drift, or identity split @@ -1530,9 +1537,9 @@ recovery scope, or a storage/recovery-owned secret source. Agent Doctor repair handoff may open an existing lifecycle flow or copy a local installer command, but it must never be reinterpreted as a restore, recovery, SMART, or cleanup action. -24. Keep backend-native platform configuration reads on the adjacent AI/runtime and platform contracts. When `internal/api/` wires native TrueNAS app config for Assistant, storage and recovery may use that runtime shape during investigation, but they must not grow a parallel recovery-local config transport or provider-shaped configuration payload. -25. Keep provider-backed poll cadence and settings-runtime health on the adjacent platform-connections contract. When shared `internal/api/` and poller wiring expose TrueNAS last-sync status, failure summaries, discovered contribution counts, manual saved-test status refresh, or platform handoff links in settings, storage and recovery may consume the resulting datasets, apps, disks, and recovery artifacts but must not redefine those settings-runtime health semantics or connection-level handoffs in storage/recovery-local transport or page flows. -26. Keep recovery filter/query state on the shared route-state parsing contract without restoring standalone recovery navigation. When platform pages or other embedded owners expose TrueNAS recovery context, they may reuse the canonical recovery query vocabulary with owned `platform` and `node` fields, but they must land inside an owning platform/runtime route instead of inventing drawer-local recovery URLs, treating PBS services as the only recovery path, or sending operators to the retired Recovery aggregate route. +22. Keep backend-native platform configuration reads on the adjacent AI/runtime and platform contracts. When `internal/api/` wires native TrueNAS app config for Assistant, storage and recovery may use that runtime shape during investigation, but they must not grow a parallel recovery-local config transport or provider-shaped configuration payload. +23. Keep provider-backed poll cadence and settings-runtime health on the adjacent platform-connections contract. When shared `internal/api/` and poller wiring expose TrueNAS last-sync status, failure summaries, discovered contribution counts, manual saved-test status refresh, or platform handoff links in settings, storage and recovery may consume the resulting datasets, apps, disks, and recovery artifacts but must not redefine those settings-runtime health semantics or connection-level handoffs in storage/recovery-local transport or page flows. +24. Keep recovery filter/query state on the shared route-state parsing contract without restoring standalone recovery navigation. When platform pages or other embedded owners expose TrueNAS recovery context, they may reuse the canonical recovery query vocabulary with owned `platform` and `node` fields, but they must land inside an owning platform/runtime route instead of inventing drawer-local recovery URLs, treating PBS services as the only recovery path, or sending operators to the retired Recovery aggregate route. That same shared route-helper boundary also owns exact workload handoffs when storage or recovery surfaces send operators back to node-scoped workloads for investigation context. Proxmox VM and system-container links @@ -1544,8 +1551,8 @@ recovery scope, or a storage/recovery-owned secret source. Patrol control anchor, must stay owned by their product surface and must not be reused as recovery entry points or storage/recovery navigation aliases. -27. Keep alert-side recovery drill-ins on that same embedded-owner route-state contract. When alert investigation surfaces such as resource-incident panels expose recovery follow-up links for TrueNAS or future API-backed platforms, they must route through an owning platform/runtime destination using canonical recovery query vocabulary instead of freezing alert-local recovery URLs, reviving the retired Recovery aggregate route, or introducing another provider-shaped recovery handoff vocabulary. -28. Keep VMware onboarding runtime and recovery semantics separate on that same adjacent platform-connections contract. When `internal/api/router.go`, `internal/api/router_routes_registration.go`, or `internal/api/vmware_handlers.go` evolve VMware connection CRUD, poller-owned `poll` / `observed` summary payloads, saved-test refresh, or observed datastore/VM snapshot visibility, storage and recovery may consume the resulting shared context but must not treat those onboarding/runtime payloads as canonical recovery artifacts, restore capability, or recovery-local control transport. +25. Keep alert-side recovery drill-ins on that same embedded-owner route-state contract. When alert investigation surfaces such as resource-incident panels expose recovery follow-up links for TrueNAS or future API-backed platforms, they must route through an owning platform/runtime destination using canonical recovery query vocabulary instead of freezing alert-local recovery URLs, reviving the retired Recovery aggregate route, or introducing another provider-shaped recovery handoff vocabulary. +26. Keep VMware onboarding runtime and recovery semantics separate on that same adjacent platform-connections contract. When `internal/api/router.go`, `internal/api/router_routes_registration.go`, or `internal/api/vmware_handlers.go` evolve VMware connection CRUD, poller-owned `poll` / `observed` summary payloads, saved-test refresh, or observed datastore/VM snapshot visibility, storage and recovery may consume the resulting shared context but must not treat those onboarding/runtime payloads as canonical recovery artifacts, restore capability, or recovery-local control transport. A successful VMware connection test with `degraded` optional signal or performance diagnostics remains connection-readiness context only; storage and recovery must not reinterpret that warning as datastore protection @@ -1556,7 +1563,7 @@ recovery scope, or a storage/recovery-owned secret source. route remains deployment-installability and API-contract owned; storage and recovery must not treat release-note availability or publication time as protection freshness, recovery evidence, or restore state. -29. Keep VMware datastore projection on the shared unified-resource and storage-source contracts. When `frontend-modern/src/hooks/useUnifiedResources.ts` or shared `internal/api/router.go` wiring starts surfacing VMware-backed canonical `storage` resources, storage and recovery may expose those datastores through the owned `vmware-vsphere` source/platform vocabulary for inventory, capacity, and handoff flows only; they must not reinterpret that projection as VMware recovery support, restore semantics, or a provider-local protection surface. +27. Keep VMware datastore projection on the shared unified-resource and storage-source contracts. When `frontend-modern/src/hooks/useUnifiedResources.ts` or shared `internal/api/router.go` wiring starts surfacing VMware-backed canonical `storage` resources, storage and recovery may expose those datastores through the owned `vmware-vsphere` source/platform vocabulary for inventory, capacity, and handoff flows only; they must not reinterpret that projection as VMware recovery support, restore semantics, or a provider-local protection surface. The same shared unified-resource boundary also covers canonical Resource.Uptime fallback on the consumer side. When `frontend-modern/src/hooks/useUnifiedResources.ts`'s `toResource` @@ -1574,7 +1581,7 @@ recovery scope, or a storage/recovery-owned secret source. health-check targets as protection topology, or container block I/O totals as backup throughput, recovery-point evidence, protection cadence, or storage-health ownership. -30. Keep VMware placement, cluster service state, guest-detail, VM snapshot-tree, VM virtual-hardware configuration, VMware Tools, VM hardware Ethernet, VM hardware disk, and network enrichment descriptive on that same shared unified-resource contract. When `internal/vmware/provider.go`, `internal/unifiedresources/types.go`, and `frontend-modern/src/hooks/useUnifiedResources.ts` project datacenter, cluster, `vmware.clusterHaEnabled`, `vmware.clusterDrsEnabled`, folder, runtime-host, datastore-attachment, guest-hostname, guest-IP, `vmware.currentSnapshotId`, `vmware.snapshotTree`, snapshot creation/state/quiesce/current markers, child snapshot metadata, `vmware.hardware`, virtual hardware version, hardware upgrade policy/version/status/error, boot type/order/retry/setup-mode flags, CPU cores-per-socket and hot-add/remove flags, memory hot-add settings, `vmware.tools`, Tools run state, version status, version number/string, install type, upgrade policy, auto-update support, install-attempt count, guest reboot requests, `vmware.networkAdapters`, adapter MAC address/type, backing network id/name, backing type, connection state, start-connected / guest-control flags, `vmware.virtualDisks`, virtual disk label/type, IDE/SCSI/SATA/NVMe placement, VMDK path, backing type, datastore name, capacity, `vmware.networkType`, `vmware.networkHostNames`, `vmware.networkVmNames`, or `vmware.tags` onto canonical VMware `agent` / `vm` / `storage` / `network` resources, storage and recovery may use that detail for labeling, navigation, and VM investigation context only; they must not promote those topology, cluster-service, guest, snapshot-tree, virtual-hardware, VMware Tools, vNIC, virtual disk, tag, or network fields into recovery ownership, restore targeting, protection grouping, compliance scoring, or a VMware-local recovery taxonomy without a separately governed slice. +28. Keep VMware placement, cluster service state, guest-detail, VM snapshot-tree, VM virtual-hardware configuration, VMware Tools, VM hardware Ethernet, VM hardware disk, and network enrichment descriptive on that same shared unified-resource contract. When `internal/vmware/provider.go`, `internal/unifiedresources/types.go`, and `frontend-modern/src/hooks/useUnifiedResources.ts` project datacenter, cluster, `vmware.clusterHaEnabled`, `vmware.clusterDrsEnabled`, folder, runtime-host, datastore-attachment, guest-hostname, guest-IP, `vmware.currentSnapshotId`, `vmware.snapshotTree`, snapshot creation/state/quiesce/current markers, child snapshot metadata, `vmware.hardware`, virtual hardware version, hardware upgrade policy/version/status/error, boot type/order/retry/setup-mode flags, CPU cores-per-socket and hot-add/remove flags, memory hot-add settings, `vmware.tools`, Tools run state, version status, version number/string, install type, upgrade policy, auto-update support, install-attempt count, guest reboot requests, `vmware.networkAdapters`, adapter MAC address/type, backing network id/name, backing type, connection state, start-connected / guest-control flags, `vmware.virtualDisks`, virtual disk label/type, IDE/SCSI/SATA/NVMe placement, VMDK path, backing type, datastore name, capacity, `vmware.networkType`, `vmware.networkHostNames`, `vmware.networkVmNames`, or `vmware.tags` onto canonical VMware `agent` / `vm` / `storage` / `network` resources, storage and recovery may use that detail for labeling, navigation, and VM investigation context only; they must not promote those topology, cluster-service, guest, snapshot-tree, virtual-hardware, VMware Tools, vNIC, virtual disk, tag, or network fields into recovery ownership, restore targeting, protection grouping, compliance scoring, or a VMware-local recovery taxonomy without a separately governed slice. `vmware.tags` is the sharpest case of that boundary, because vCenter tag vocabularies routinely read like backup policy — an operator-authored `Backup:Nightly` or `Compliance:PCI` is a label the operator wrote, not a @@ -1583,7 +1590,7 @@ recovery scope, or a storage/recovery-owned secret source. for an observed backup, satisfy a protection requirement, or move a resource's coverage or compliance verdict; those verdicts continue to come from recovery-owned evidence alone. -31. Keep VMware datastore classification neutral on the shared storage adapter contract. When `frontend-modern/src/features/storageBackups/resourceStorageMapping.ts`, `frontend-modern/src/features/storageBackups/resourceStoragePresentation.ts`, and `frontend-modern/src/features/storageBackups/storageAdapters.ts` evolve canonical storage-record mapping, VMware-backed datastores must continue to land on the shared storage route as inventory-only datastores with neutral protection fallback, not as backup repositories, backup targets, or recovery-protected resources. +29. Keep VMware datastore classification neutral on the shared storage adapter contract. When `frontend-modern/src/features/storageBackups/resourceStorageMapping.ts`, `frontend-modern/src/features/storageBackups/resourceStoragePresentation.ts`, and `frontend-modern/src/features/storageBackups/storageAdapters.ts` evolve canonical storage-record mapping, VMware-backed datastores must continue to land on the shared storage route as inventory-only datastores with neutral protection fallback, not as backup repositories, backup targets, or recovery-protected resources. That same shared storage adapter boundary also owns canonical platform family vocabulary through the governed platform manifest. `frontend-modern/src/features/storageBackups/models.ts`, @@ -1601,7 +1608,7 @@ recovery scope, or a storage/recovery-owned secret source. recovery platform. Shared `AgentData.hostProfile` may carry that profile id for presentation, while storage and recovery must continue to treat `AgentData.platform` as the normalized runtime platform. -32. Keep agentless availability endpoints neutral on the shared unified-resource and API contracts. When `internal/api/availability_handlers.go`, `internal/api/connections_handlers.go`, `internal/api/platform_mock_connections.go`, or `frontend-modern/src/hooks/useUnifiedResources.ts` surface `network-endpoint` availability resources, storage and recovery may consume their liveness as infrastructure context only; they must not reinterpret ping/TCP/HTTP endpoints as storage providers, backup targets, recovery repositories, or protected-workload evidence. +30. Keep agentless availability endpoints neutral on the shared unified-resource and API contracts. When `internal/api/availability_handlers.go`, `internal/api/connections_handlers.go`, `internal/api/platform_mock_connections.go`, or `frontend-modern/src/hooks/useUnifiedResources.ts` surface `network-endpoint` availability resources, storage and recovery may consume their liveness as infrastructure context only; they must not reinterpret ping/TCP/HTTP endpoints as storage providers, backup targets, recovery repositories, or protected-workload evidence. The same neutrality applies when one or more canonical `availabilityChecks` facets attach to a storage-related resource. The additive plural facet, its compatibility `availability` summary, evidence @@ -1618,13 +1625,13 @@ recovery scope, or a storage/recovery-owned secret source. `certificate_*` incident on those availability facets remain monitoring and alerts context. They do not prove storage encryption, backup integrity, repository authenticity, protection coverage, or restore readiness. -33. Keep infrastructure summary chart bucketing and short response caching presentation-only on the adjacent shared API boundary. When `internal/api/chartapi/service.go` normalizes mixed-cadence infrastructure history into equal-time summary buckets or serves a cached summary payload for repeated operator-facing summary-card requests, storage and recovery may consume the resulting visual context only; they must not reinterpret those normalized chart samples, cached timestamps, or cache hits as recovery freshness windows, backup cadence, or restore evidence. +31. Keep infrastructure summary chart bucketing and short response caching presentation-only on the adjacent shared API boundary. When `internal/api/chartapi/service.go` normalizes mixed-cadence infrastructure history into equal-time summary buckets or serves a cached summary payload for repeated operator-facing summary-card requests, storage and recovery may consume the resulting visual context only; they must not reinterpret those normalized chart samples, cached timestamps, or cache hits as recovery freshness windows, backup cadence, or restore evidence. The same router may wire the adjacent server-owned agent command authorization verifier, but storage/recovery must not treat an approval id, signed command grant, or command result as restore authorization or recovery evidence; that authority remains action-governance and agent-lifecycle owned. -34. Keep workload chart downsampling and short response caching presentation-only on that same adjacent shared API boundary. When `internal/api/chartapi/service.go` caps mixed-cadence workload history into equal-time buckets or serves a cached workload-summary payload for repeated operator-facing workload-card requests, storage and recovery may consume the resulting visual context only; they must not reinterpret those shaped chart samples, cached timestamps, or cache hits as recovery freshness windows, backup cadence, or restore evidence. +32. Keep workload chart downsampling and short response caching presentation-only on that same adjacent shared API boundary. When `internal/api/chartapi/service.go` caps mixed-cadence workload history into equal-time buckets or serves a cached workload-summary payload for repeated operator-facing workload-card requests, storage and recovery may consume the resulting visual context only; they must not reinterpret those shaped chart samples, cached timestamps, or cache hits as recovery freshness windows, backup cadence, or restore evidence. The same adjacent chart boundary now covers compact storage capacity transport. `internal/api/chartapi/service.go` may batch only the canonical `used` and `avail` storage series for `/api/charts/storage-summary`, but @@ -1639,7 +1646,7 @@ recovery scope, or a storage/recovery-owned secret source. In mock mode, that same compact route must stay aggregate-only and sampler-prewarmed; storage and recovery must not trigger per-pool chart reconstruction on the first dashboard request after each mock refresh. -36. Keep shared `frontend-modern/src/App.tsx` public-route ownership explicit by +33. Keep shared `frontend-modern/src/App.tsx` public-route ownership explicit by surface. Storage/recovery preview entrypoints such as `/preview/setup-complete` may remain public app-shell routes, but unrelated commercial compatibility handoffs like `/pricing` must stay separate thin @@ -1694,7 +1701,7 @@ recovery scope, or a storage/recovery-owned secret source. owners must treat that as shell-owned navigation evidence, not as a reason to restore Storage or Recovery as equal primary tabs or depend on platform-only tab terminology. -37. Keep public self-hosted purchase handoff and activation routes on the +34. Keep public self-hosted purchase handoff and activation routes on the adjacent commercial/auth boundary. When `internal/api/router.go`, `internal/api/router_routes_cloud.go`, `internal/api/licensing_handlers.go`, or `internal/api/demo_mode_commercial.go` evolve @@ -1747,14 +1754,14 @@ recovery scope, or a storage/recovery-owned secret source. runtime as populated mock inventory, but they must not expose `demo_fixtures`, billing identity, or alternate entitlement semantics as recovery-local transport or operator-facing storage metadata. -39. Keep storage and recovery route framing additive and owner-neutral. +35. Keep storage and recovery route framing additive and owner-neutral. `frontend-modern/src/components/Storage/Storage.tsx` and storage/recovery- adjacent route composition may use the shared `PageHeader` shell for top-level route framing, but that header must stay additive on top of the canonical storage page model, recovery presenters, and shared summary caches. Header chrome must not become a second owner for storage filters, recovery posture, commercial purchase state, or transport selection. -40. Keep the unified connections ledger owner-neutral toward storage and +36. Keep the unified connections ledger owner-neutral toward storage and recovery. Shared `internal/api/router.go` may mount the `/api/connections` and `/api/connections/probe` routes alongside the existing storage/recovery-adjacent API surfaces, and @@ -1807,7 +1814,7 @@ recovery scope, or a storage/recovery-owned secret source. Storage filter option semantics stay storage-owned, but FilterBar chip presentation is frontend-primitives-owned: storage status leading dots must use `filterChipStatusDot` rather than storage-local span factories. -41. Keep agent memory composition descriptive on the shared unified-resource +37. Keep agent memory composition descriptive on the shared unified-resource contract. `internal/unifiedresources/types.go` carries the reclaimable page-cache split (`AgentMemoryMeta.cache`, holding used + cache + free within the reported total) as host RAM description for machine surfaces. @@ -1984,24 +1991,24 @@ files. appliance, or recovery endpoint was read, mutated, or verified. The operations-loop status projection is also adjacent-only. Its content-free stage, next-action, Patrol evidence, contextual collaboration, pending -approval, governed action, verified outcome, and optional token-backed MCP -readiness fields may describe Pulse Intelligence activation progress, but storage and -recovery must not treat them as backup coverage, restore readiness, storage -health verification, appliance access, dataset access, API-token authority -for recovery paths, or recovery mutation proof. -If an aggregate active Patrol finding or pending approval outranks older -completed/resolved loop proof in that projection, that precedence remains only -current operator orientation; it is not backup freshness, restore authority, or -storage-local remediation proof. -Operations-loop workflow starter request counts are even narrower: they are -content-free markers that a native Assistant surface rendered, a first-party -Patrol control handoff started, a legacy Pro activation entry-point handoff -started, or a Pulse MCP surface rendered the manifest-owned -`pulse_operations_loop` starter. The aggregate Patrol control starter count may -include native Patrol, legacy Patrol autonomy, and legacy Pro activation starts, -while `proActivationOperationsLoopStarterCount` remains the legacy entry-point -count. Storage and recovery may observe those aggregate activation reports, but -must not treat starter + approval, governed action, verified outcome, and optional token-backed MCP + readiness fields may describe Pulse Intelligence activation progress, but storage and + recovery must not treat them as backup coverage, restore readiness, storage + health verification, appliance access, dataset access, API-token authority + for recovery paths, or recovery mutation proof. + If an aggregate active Patrol finding or pending approval outranks older + completed/resolved loop proof in that projection, that precedence remains only + current operator orientation; it is not backup freshness, restore authority, or + storage-local remediation proof. + Operations-loop workflow starter request counts are even narrower: they are + content-free markers that a native Assistant surface rendered, a first-party + Patrol control handoff started, a legacy Pro activation entry-point handoff + started, or a Pulse MCP surface rendered the manifest-owned + `pulse_operations_loop` starter. The aggregate Patrol control starter count may + include native Patrol, legacy Patrol autonomy, and legacy Pro activation starts, + while `proActivationOperationsLoopStarterCount` remains the legacy entry-point + count. Storage and recovery may observe those aggregate activation reports, but + must not treat starter access as backup coverage, recovery freshness, restore readiness, storage health verification, dataset access, appliance access, or evidence that any recovery endpoint was used. @@ -2104,7 +2111,7 @@ must not treat starter behavior by fanning out per-pool `/api/metrics-store/history` reads, by pulling the full storage-page `/api/storage-charts` payload, or by inventing a dashboard-only storage history transport. -15a. Keep shared diagnostics cache scope honest when storage/recovery-adjacent + 15a. Keep shared diagnostics cache scope honest when storage/recovery-adjacent surfaces reuse `internal/api/diagnostics.go`. The shared diagnostics payload must not include local commercial funnel summaries or infrastructure-onboarding analytics, so recovery-adjacent diagnostics do @@ -2120,7 +2127,7 @@ must not treat starter load or save AI settings through shared helpers, any historical hosted quickstart model IDs must be cleared before adjacent surfaces read or re-emit that state. Legacy Anthropic OAuth tokens follow the same shared-owner rule: adjacent storage/recovery code may not use them as provider configuration and must leave cleanup to the AI settings contract. -17a. Keep adjacent AI paid-control state entitlement-effective on that shared + 17a. Keep adjacent AI paid-control state entitlement-effective on that shared `internal/api/` boundary. Storage- and recovery-adjacent flows may preserve stored Assistant or Patrol preferences in config, but they must not treat stored autonomous, auto-remediation, or alert-triggered analysis settings @@ -2260,8 +2267,8 @@ capability over storage or recovery data. pool reads as its concrete layout ("Mirror", "Raidz2") without the discriminator ever carrying presentation detail. Regression coverage: `labels a TrueNAS pool by its vdev layout while keeping the pool - discriminator` and `falls back to the topology discriminator when no - vdev layout is reported` in +discriminator` and `falls back to the topology discriminator when no +vdev layout is reported` in `frontend-modern/src/features/storageBackups/__tests__/storageAdapters.test.ts`. 27. A rejected legacy RBAC import must not destroy the store. The import is @@ -5270,6 +5277,7 @@ The ZFS pool-detail summary has one stable shape: `datasets` is always an array, including when the backend reports no dataset inventory. Consumers and branch proofs must retain the empty array instead of treating dataset support as an optional return-field contract. + ### Scheduled reporting adoption is read from persisted config as counts The usage telemetry snapshot reads `LoadReportScheduleStore` and reports three @@ -5283,6 +5291,7 @@ reporting actually produced output, not whether it is currently armed. The read is in `applyLicensedFeatureConfigSnapshot` (`pkg/server/telemetry_licensed_features.go`), pinned by `TestApplyLicensedFeatureConfigSnapshot_CountsScheduledReportingAndProfiles`. + ### Audit-read activity is a bounded, content-free local history `audit_read_activity.json` follows the same shape as the existing external-agent @@ -5294,6 +5303,7 @@ the 30-day telemetry window are excluded at count time by `CountAuditReadActivitySince`, so the derived signal stays "is using" rather than drifting into "has ever used" (`TestAuditReadsOutsideWindowAreNotCounted`). + ### Per-tenant resource stores are released on offboarding and shutdown `ResourceHandlers.getStore` opens a SQLite handle per org and caches it for the @@ -5307,6 +5317,7 @@ a fresh handle rather than using a closed one (`TestResourceHandlers_CloseTenantStoreReleasesTheHandle`), and both entry points are idempotent and nil-safe (`TestResourceHandlers_CloseIsIdempotentAndNilSafe`). + ### Report-ack version echo persists nothing The unified-agent report ack's `serverVersion` field is supplied from the @@ -5315,6 +5326,7 @@ running server's version at router wiring time ingest path's persistence surface — agent-id continuity and host state — is unchanged by the echo (`TestHostAgentRemovalLifecycleThroughAuthenticatedRouterAndRestart`). + ### Security status capability payload gained an infrastructure field `internal/api/` is a canonical reference in this contract's Extension Points, so diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 653dd042e..7637b41f4 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -380,7 +380,11 @@ consumers use `platform-table-narrow-hidden` to demote only their lowest-value field while the shared shell promotes identity to 40 percent and retains five readable fields. Provider tables must keep summary rows single-line and may truncate a value only when the row exposes its complete source value through a -touch- and keyboard-operable inline detail disclosure. +touch- and keyboard-operable inline detail disclosure. The frontend-primitives +owner fixes that summary rhythm at 32 pixels for every `platform-table` path; +provider consumers must not add stacked subtitles or local row-height +exceptions. Lower-priority provider metadata belongs in another operational +column, a supplemental tooltip, or the inline detail drawer. Future platform tables must keep that split: row data and platform semantics stay in the unified-resource consumer, and the repeated table shell stays in the shared frontend primitive. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 4f808f099..37b26060f 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,37 +1,64 @@ { "version": 1, - "base_sha": "6991208ad62c550328109f50ea53b7b4202da62b", - "verified_at": "2026-08-24T14:36:00Z", + "base_sha": "7e363e40e7aa21bf4d5029108a217c0f93b963b5", + "verified_at": "2026-08-24T15:09:06Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx", - "frontend-modern/src/components/Workloads/GuestDrawer.tsx", - "frontend-modern/src/components/Workloads/NodeDrawer.tsx", - "frontend-modern/src/components/Workloads/WorkloadPanel.tsx", - "frontend-modern/src/components/shared/DetailSectionTable.tsx", - "frontend-modern/src/components/shared/ObjectDrawerHeader.tsx", - "frontend-modern/src/features/docker/DockerHostDrawer.tsx", - "frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx", - "frontend-modern/src/features/docker/DockerHostsTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx", - "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx" + "frontend-modern/src/features/docker/DockerAlertsTable.tsx", + "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx", + "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx", + "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx", + "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx", + "frontend-modern/src/features/truenas/TrueNASAppsTable.tsx", + "frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx", + "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx", + "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx", + "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx", + "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx", + "frontend-modern/src/features/vmware/VsphereActivityTable.tsx", + "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx", + "frontend-modern/src/index.css" ], "content_sha256": { - "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx": "65b714d5e11f32aad5baf27dab6529a1759478e86be377ae4e208fd3730a0815", - "frontend-modern/src/components/Workloads/GuestDrawer.tsx": "d13a9e66746541b7c28311d2e07ba961df4620c2178ab8fc5ebc1a079a858263", - "frontend-modern/src/components/Workloads/NodeDrawer.tsx": "14926ce4b200435a7675028d5467aed89b5c9cb894e788822ff9fd70ae7f1f6f", - "frontend-modern/src/components/Workloads/WorkloadPanel.tsx": "b933c323e93f56022f57d8083e5e2b4d715e8de906d7d3d38c7802ef1680b0c4", - "frontend-modern/src/components/shared/DetailSectionTable.tsx": "9ea698e10d5ef197a5e0bea3cc5a97a23173116b093cb4377a0bcbd29119fb20", - "frontend-modern/src/components/shared/ObjectDrawerHeader.tsx": "39702f2a07f9f3c9b64a82820ce21f6e42ba1b88b33e7900ebb353fab09cce83", - "frontend-modern/src/features/docker/DockerHostDrawer.tsx": "e05dcca8cd2c9a170bc6a405d869982a6e6607d760083f62b95520cc131358ab", - "frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx": "6ad83f0faf4c02fb29f027104ff91cc6fbafd8fc0db8da1693f1546e00bdca9b", - "frontend-modern/src/features/docker/DockerHostsTable.tsx": "c6af64cb0a2fbdf2d1973463b1a7d992685e39bda8456a138ed1ff81e2c71718", - "frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx": "4da690f8ac74e95d77f728f7be910e140e25119f796e0874b2bbd6193e4cf5fc", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx": "e0eba7b1fa5233379bb7f0eda3e49920976a5b9b82fa4749f8e150f186f3a687", - "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "a192b907ce195f9c72257d25f0cf1e015a070e30cea794872dbeda2f4d2b1826" + "frontend-modern/src/features/docker/DockerAlertsTable.tsx": "c9cee8ac612f616810964394ca2873c44bc36abe5313a61e3d26a74579c9f1d5", + "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx": "3840ec971c93e89fe6842d942493b4811cf7b6c76a9c66b28c3480be14ecf304", + "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "0a76cb318523582bb1342ff23161041642ffcf5259c2205d127df11ed68e7584", + "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "efa5326b0bd8562348107513c632c44aa828eb03942b056acf2f53b25da3301b", + "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx": "22cdce43826f6543f0bf295099a4362c3428d80a8781064b7a3249323204ad06", + "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "216a0e0576145864500f4b975d5699a3fe6abc4fa226664f7522a19205030fa2", + "frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx": "8703f76bb5decb5a80a17d6cb221443d91691fb513619bd39bfd771092e66edc", + "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx": "3973e4bbb836c796d558f298b6c5c6e30ee36c608923ffe030118f23c2c101b6", + "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx": "87a3793e3871abfa534bff3220a433cf7e6ae75a658fd93813e8cde37af2da3f", + "frontend-modern/src/features/truenas/TrueNASAppsTable.tsx": "c14a06d7087127289391b8a8e5af62ea88e1f30aac23c57e0994bc149c0108d3", + "frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx": "3994bd01b6cd723bfacabf5edcd1afad7b5f23be5c15f8558f6efe74b4664a3f", + "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx": "f14b8373ac11cd9f7039032554c4d541bd69a66b9969e7d26976264ec6e18ef9", + "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "f36e5afd440d4f440eb72f1594842121c4abc32dfcff90305a3a538453724b19", + "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx": "a24d04e5ca1f9c6a9a4cdee795866cdd9069d93fa657aa7e3b65a0a4414b6711", + "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx": "ce44e4f4686a23d44999f6b8e035b46d30a7651c7aa3d978d560a0ac834318e5", + "frontend-modern/src/features/vmware/VsphereActivityTable.tsx": "df0c19160f33f5c23129317d941dd003d26d52edc5fda503475f3a07631571ca", + "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx": "76de93dde01009102aedbab381833530199e31861f82a1469689639d5ada264d", + "frontend-modern/src/index.css": "ee23f0612d3a28e74d1bd260709616b3e54b9836451a618d8f55dfb21d7bcfd4" }, - "routes": ["/proxmox/overview", "/docker/overview"], + "routes": [ + "/proxmox/overview", + "/proxmox/backups/date", + "/proxmox/ceph", + "/docker/overview", + "/kubernetes/overview", + "/truenas/overview", + "/truenas/storage", + "/truenas/apps", + "/truenas/vms", + "/truenas/shares", + "/truenas/protection", + "/vmware/health", + "/vmware/activity", + "/standalone/machines" + ], "viewports": [ { "width": 1280, @@ -43,16 +70,15 @@ } ], "states": [ - "Proxmox node drawer with the complete subject header row presented as one visible collapse target at desktop and phone widths", - "Proxmox guest drawer with the same canonical full-width collapse affordance at phone width", - "Proxmox Ceph cluster and Mail Gateway drawers with the same canonical full-width collapse affordance at phone width", - "Docker host drawer with the canonical full-width collapse affordance and condensed technical rows at phone width" + "single-line platform summary rows across Proxmox, Docker, Kubernetes, TrueNAS, vSphere, and standalone machine tables", + "Proxmox node, guest, backup-server, Ceph, TrueNAS alert, app, VM, share, protection, storage, vSphere alert/activity, and standalone machine identities without stacked subtitles", + "default and expanded Proxmox node states with the inline detail row remaining content-sized", + "desktop and narrow table layouts with provider-specific column priority preserved" ], "interactions": [ - "expanded and collapsed a Proxmox node by pressing the full subject header row at desktop and phone widths", - "expanded and collapsed a Proxmox guest by pressing the full subject header row at phone width", - "expanded and collapsed Ceph cluster and Mail Gateway objects by pressing their full subject header rows at phone width", - "expanded and collapsed a Docker host by pressing the full subject header row at phone width", - "confirmed the visible subject content and chevron communicate the full-row collapse affordance without adding a separate Close control" + "expanded a Proxmox node from its canonical 32px summary row and collapsed it from the full drawer header", + "navigated every changed provider route at narrow width and inspected row height, truncation, clipping, horizontal containment, and bottom-navigation clearance", + "inspected representative Proxmox, Docker, TrueNAS, vSphere, and standalone tables at desktop width", + "confirmed supplemental provider metadata remains available through inline details or cell titles without creating a second visual line" ] } diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index a7c9e7a69..fe72b03f7 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -3847,7 +3847,7 @@ { "id": "platform-table-shell-local-frame", "category": "platform-table-shell", - "summary": "Platform tables must compose the shared PlatformTableShell frame instead of recreating the TableCard/TableHeader/TableBody shell locally.", + "summary": "Platform tables must compose the shared PlatformTableShell frame and its canonical single-line 32px summary-row rhythm instead of recreating local table chrome or provider-specific density.", "canonical": { "path": "src/features/platformPage/sharedPlatformPage.tsx", "export": "PlatformTableShell" diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts index 8707781ef..51dd9e551 100644 --- a/frontend-modern/src/__tests__/App.architecture.test.ts +++ b/frontend-modern/src/__tests__/App.architecture.test.ts @@ -560,7 +560,12 @@ describe('App architecture', () => { expect(appStylesSource).toContain('.pulse-wide-data-surface.space-y-3'); expect(appStylesSource).toContain('.filter-bar > div > div:first-child button'); expect(appStylesSource).not.toContain('.proxmox-nodes-card > :first-child'); - expect(appStylesSource).toContain('.pulse-wide-data-surface .host-row'); + expect(appStylesSource).toContain( + '.table-fixed.platform-table > tbody > tr:not([data-inline-detail-for])', + ); + expect(appStylesSource).not.toContain( + '.pulse-wide-data-surface .host-row,\n .pulse-wide-data-surface .workload-row', + ); expect(appStylesSource).toContain('.pulse-footer > div'); expect(appLayoutSource).toContain('tabs mb-2 hidden xl:flex'); expect(appLayoutSource).toContain('xl:px-2 2xl:px-3'); diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 56bed19d3..48bb170b7 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -5267,6 +5267,12 @@ describe('shared primitive guardrails', () => { expect(registeredGuard?.canonical?.export).toBe('PlatformTableShell'); expect(registeredGuard?.allowedPaths ?? []).toHaveLength(0); expect(sharedPlatformPageSource).toContain('export function PlatformTableShell'); + expect(sharedPlatformPageSource).toContain( + "export const PLATFORM_TABLE_SUMMARY_ROW_CLASS = 'h-8'", + ); + expect(sharedPlatformPageSource).toContain( + 'getPlatformTableRowClass = (): string => PLATFORM_TABLE_SUMMARY_ROW_CLASS', + ); expect(sharedPlatformPageSource).toContain('TableCard class={props.cardClass'); expect(sharedPlatformPageSource).toContain('TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}'); expect(sharedPlatformPageSource).toContain('TableBody class={PLATFORM_TABLE_BODY_CLASS}'); @@ -5295,6 +5301,40 @@ describe('shared primitive guardrails', () => { } }); + it('keeps provider summary rows on the canonical single-line density contract', () => { + expect(frontendIndexCssSource).toContain( + '.table-fixed.platform-table > tbody > tr:not([data-inline-detail-for])', + ); + expect(frontendIndexCssSource).toContain('height: 2rem;'); + expect(frontendIndexCssSource).not.toContain( + '.pulse-wide-data-surface .host-row,\n .pulse-wide-data-surface .workload-row', + ); + + for (const source of [ + dockerAlertsTableSource, + kubernetesAlertsTableSource, + proxmoxBackupServersTableSource, + proxmoxCephTableSource, + proxmoxNodesTableSource, + proxmoxRecoverableTableSource, + agentsMachinesTableSource, + truenasAlertsTableSource, + truenasAppsTableSource, + truenasNetworkSharesTableSource, + truenasProtectionTableSource, + truenasStorageTopologyTableSource, + truenasSystemsTableSource, + truenasVirtualMachinesTableSource, + vsphereActivityTableSource, + vsphereAlertsTableSource, + ]) { + expect(source).not.toContain('block truncate text-[10px] text-muted'); + expect(source).not.toContain('flex min-w-0 flex-1 flex-col'); + } + + expect(agentsMachinesTableSource).not.toContain('grid-rows-2'); + }); + it('keeps platform count grammar and route status normalization shared', () => { expect(sharedPlatformPageSource).toContain('export const getPlatformResourceCountNoun'); expect(sharedPlatformPageSource).toContain( diff --git a/frontend-modern/src/features/docker/DockerAlertsTable.tsx b/frontend-modern/src/features/docker/DockerAlertsTable.tsx index 8cf67dd71..7584e5287 100644 --- a/frontend-modern/src/features/docker/DockerAlertsTable.tsx +++ b/frontend-modern/src/features/docker/DockerAlertsTable.tsx @@ -231,16 +231,21 @@ export const DockerAlertsTable: Component<{
{incident.resourceName}
-
- {formatPlatformAlertResourceType(incident.resourceType, 'docker')} - - on {incident.resource.parentName} - -
@@ -251,24 +256,26 @@ export const DockerAlertsTable: Component<{ /> - + {incident.summary} - - {incident.label} - {docker()?.hostname || docker()?.swarm?.clusterName || '-'} - - {docker()?.runtime || - formatPlatformAlertCode(incident.code, 'docker')} -
{incident.resourceName}
-
- {formatPlatformAlertResourceType( - incident.resourceType, - 'kubernetes', - )} - - · {k()?.ownerKind}/{k()?.ownerName} - -
@@ -264,21 +266,26 @@ export const KubernetesAlertsTable: Component<{ /> - + {incident.summary} - - {incident.label} - - + {scopeText()} - - {k()?.nodeName || - formatPlatformAlertCode(incident.code, 'kubernetes')} - getPlatformTableCellClass(getPlatformColumnAlign(kind)); -// Text-only rows otherwise collapse below the scanning rhythm established by -// platform rows with buttons, badges, or progress bars. -export const getPlatformTableRowClass = (): string => 'h-8'; +// Summary rows stay on one 32px scanning rhythm across every provider. Detail +// rows are deliberately excluded at the shared CSS boundary so their content +// can expand without weakening the overview-table contract. +export const getPlatformTableRowClass = (): string => PLATFORM_TABLE_SUMMARY_ROW_CLASS; export function PlatformResponsiveTableLabel(props: { compact: string; full: string }) { return ( diff --git a/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx index a1e320527..f2ca4a985 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx @@ -262,6 +262,7 @@ export function ProxmoxBackupServersTable(props: {
- - {(datastore) => ( -
- {datastore().name} -
- )} -
diff --git a/frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx b/frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx index 59b64d15d..ed33a8755 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx @@ -426,7 +426,10 @@ export const ProxmoxCephTable: Component<{ aria-expanded={isOpen()} tabIndex={0} > - +
- - -
- +
{ind.label}
- - -
} body={ - + {(node) => { const name = () => asTrimmedString(node.name) || node.id; const nativeNodeName = () => asTrimmedString(node.proxmox?.nodeName) ?? ''; @@ -366,10 +366,6 @@ export const ProxmoxNodesTable: Component<{ layoutMode() === 'narrow' && nativeNodeName().length > 0 ? nativeNodeName() : name(); - const showNativeNodeName = () => - layoutMode() === 'phone' && - nativeNodeName().length > 0 && - nativeNodeName() !== name(); const drawerNode = createMemo(() => nodeFromResource(node)); const detailRowId = () => `proxmox-host-drawer-${node.id}`; const isSelected = () => selectedNodeId() === node.id; @@ -498,21 +494,20 @@ export const ProxmoxNodesTable: Component<{ title={node.status || 'unknown'} ariaHidden /> -
+
a]:hidden' : ''}`} nameClass="truncate font-semibold text-base-content" - title={`Open ${name()} web interface`} + title={ + nativeNodeName() && nativeNodeName() !== name() + ? `${name()} · Proxmox node ${nativeNodeName()}` + : `Open ${name()} web interface` + } > {visibleNodeLabel()} - - - {nativeNodeName()} - -
diff --git a/frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx b/frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx index 24ca5f83e..fba793bb7 100644 --- a/frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx @@ -277,15 +277,20 @@ export const TrueNASNetworkSharesTable: Component<{ title={indicator().label} />
-
+
{name()}
-
- {share()?.dataset || - share()?.path || - resource.parentName || - 'TrueNAS'} -
@@ -309,11 +314,6 @@ export const TrueNASNetworkSharesTable: Component<{ title={[access().title, security().title].filter(Boolean).join(' | ')} > {access().label} - - - {security().label} - -
- + {artifact()} - - - {artifactSecondary()} - - - + {target()} - - - {targetSecondary()} - -
-
+
{name()}
-
- {subtitle(props.row)} -
); diff --git a/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx b/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx index 15da9aa57..017a510b6 100644 --- a/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx @@ -380,15 +380,10 @@ export const TrueNASSystemsTable: Component<{
{name()}
- -
- {systemMeta()} -
-
@@ -443,14 +438,11 @@ export const TrueNASSystemsTable: Component<{
@@ -260,33 +265,40 @@ export const VsphereAlertsTable: Component<{ /> - + {incident.summary} - - {incident.label} - {meta()?.connectionName || meta()?.vcenterHost || '-'} - - {meta()?.datacenterName || meta()?.clusterName || '-'} - { expect(within(table).getByText('vCenter')).toBeInTheDocument(); expect(screen.getAllByText('warehouse-api-01')).toHaveLength(2); expect(screen.getByText('Reconfigure virtual machine')).toBeInTheDocument(); - expect(screen.getByText('Permission denied while reconfiguring VM')).toBeInTheDocument(); + expect( + screen.getByTitle('Reconfigure virtual machine · Permission denied while reconfiguring VM'), + ).toBeInTheDocument(); expect(screen.getByText('Error')).toBeInTheDocument(); expect(screen.getByText('VmPoweredOnEvent')).toBeInTheDocument(); expect(screen.getByText('administrator@vsphere.local')).toBeInTheDocument(); @@ -119,7 +121,7 @@ describe('VsphereActivityTable', () => { expect(detail.getByText('Description')).toBeInTheDocument(); expect(detail.getByText('Reconfigure virtual machine CPU reservation')).toBeInTheDocument(); - await fireEvent.click(detail.getByRole('button', { name: 'Close' })); + await fireEvent.click(detail.getByRole('button', { name: /^Collapse .* details$/ })); expect(screen.queryByTestId('vsphere-activity-detail')).not.toBeInTheDocument(); expect(row).toHaveAttribute('aria-expanded', 'false'); diff --git a/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx b/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx index 16a86d69b..6a91c6f3a 100644 --- a/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx +++ b/frontend-modern/src/features/vmware/__tests__/VsphereAlertsTable.test.tsx @@ -89,7 +89,7 @@ describe('VsphereAlertsTable', () => { const firstRow = document.querySelector('[data-vsphere-alert-row]') as HTMLElement; expect(firstRow).toHaveAttribute('data-vsphere-alert-row', 'host-alarm:incident:alarm-401:0'); expect(within(firstRow).getByText('lab-vcenter')).toBeInTheDocument(); - expect(within(firstRow).getByText('host-101')).toBeInTheDocument(); + expect(within(firstRow).getByTitle('Host · host-101')).toBeInTheDocument(); const attentionFilter = screen.getByRole('button', { name: 'Attention, 1', @@ -119,7 +119,7 @@ describe('VsphereAlertsTable', () => { expect(detail.getByText('Action')).toBeInTheDocument(); expect(detail.getByText('Investigate in vCenter')).toBeInTheDocument(); - await fireEvent.click(detail.getByRole('button', { name: 'Close' })); + await fireEvent.click(detail.getByRole('button', { name: /^Collapse .* details$/ })); expect(screen.queryByTestId('vsphere-alert-detail')).not.toBeInTheDocument(); expect(row).toHaveAttribute('aria-expanded', 'false'); diff --git a/frontend-modern/src/index.css b/frontend-modern/src/index.css index 9334fb792..bc05cbfb6 100644 --- a/frontend-modern/src/index.css +++ b/frontend-modern/src/index.css @@ -526,11 +526,6 @@ line-height: 1rem; } - .pulse-wide-data-surface .host-row, - .pulse-wide-data-surface .workload-row { - height: 2.25rem; - } - .pulse-wide-data-surface .host-row > td:first-child > div, .pulse-wide-data-surface .workload-row [data-workload-col='name'] > div { gap: 0.25rem; @@ -1023,6 +1018,16 @@ body, letter-spacing: 0.025em; } +/* + * Docker's single-line rows are the canonical operator density. Every + * platform summary row uses the same 32px scan rhythm; inline drawers remain + * content-sized. Secondary context belongs in another column, a tooltip, or + * the expanded detail row rather than making the overview row taller. + */ +.table-scroll-shell > .table-fixed.platform-table > tbody > tr:not([data-inline-detail-for]) { + height: 2rem; +} + .platform-table-label-compact { display: inline; }