mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Canonicalize platform table row density
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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-<canonical-scope-slug>` 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-<canonical-scope-slug>` 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -231,16 +231,21 @@ export const DockerAlertsTable: Component<{
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="truncate font-medium text-base-content"
|
||||
title={incident.resourceName}
|
||||
title={[
|
||||
incident.resourceName,
|
||||
formatPlatformAlertResourceType(
|
||||
incident.resourceType,
|
||||
'docker',
|
||||
),
|
||||
incident.resource.parentName
|
||||
? `on ${incident.resource.parentName}`
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{incident.resourceName}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-muted">
|
||||
{formatPlatformAlertResourceType(incident.resourceType, 'docker')}
|
||||
<Show when={incident.resource.parentName}>
|
||||
on {incident.resource.parentName}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -251,24 +256,26 @@ export const DockerAlertsTable: Component<{
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
<span class="block truncate text-base-content" title={incident.summary}>
|
||||
<span
|
||||
class="block truncate text-base-content"
|
||||
title={[incident.summary, incident.label].filter(Boolean).join(' · ')}
|
||||
>
|
||||
{incident.summary}
|
||||
</span>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{incident.label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('text')}`}>
|
||||
<span
|
||||
class="block truncate text-base-content"
|
||||
title={docker()?.hostname || docker()?.swarm?.clusterName}
|
||||
title={[
|
||||
docker()?.hostname || docker()?.swarm?.clusterName,
|
||||
docker()?.runtime ||
|
||||
formatPlatformAlertCode(incident.code, 'docker'),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{docker()?.hostname || docker()?.swarm?.clusterName || '-'}
|
||||
</span>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{docker()?.runtime ||
|
||||
formatPlatformAlertCode(incident.code, 'docker')}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} text-base-content`}
|
||||
|
||||
@@ -241,19 +241,21 @@ export const KubernetesAlertsTable: Component<{
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="truncate font-medium text-base-content"
|
||||
title={incident.resourceName}
|
||||
title={[
|
||||
incident.resourceName,
|
||||
formatPlatformAlertResourceType(
|
||||
incident.resourceType,
|
||||
'kubernetes',
|
||||
),
|
||||
k()?.ownerKind && k()?.ownerName
|
||||
? `${k()?.ownerKind}/${k()?.ownerName}`
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{incident.resourceName}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-muted">
|
||||
{formatPlatformAlertResourceType(
|
||||
incident.resourceType,
|
||||
'kubernetes',
|
||||
)}
|
||||
<Show when={k()?.ownerKind && k()?.ownerName}>
|
||||
· {k()?.ownerKind}/{k()?.ownerName}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -264,21 +266,26 @@ export const KubernetesAlertsTable: Component<{
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
<span class="block truncate text-base-content" title={incident.summary}>
|
||||
<span
|
||||
class="block truncate text-base-content"
|
||||
title={[incident.summary, incident.label].filter(Boolean).join(' · ')}
|
||||
>
|
||||
{incident.summary}
|
||||
</span>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{incident.label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
<span class="block truncate text-base-content" title={scopeText()}>
|
||||
<span
|
||||
class="block truncate text-base-content"
|
||||
title={[
|
||||
scopeText(),
|
||||
k()?.nodeName ||
|
||||
formatPlatformAlertCode(incident.code, 'kubernetes'),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{scopeText()}
|
||||
</span>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{k()?.nodeName ||
|
||||
formatPlatformAlertCode(incident.code, 'kubernetes')}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
|
||||
|
||||
@@ -243,6 +243,7 @@ export const PLATFORM_TABLE_CARD_CLASS = 'rounded-md';
|
||||
export const PLATFORM_TABLE_HEADER_ROW_CLASS = 'bg-surface-alt text-muted border-b border-border';
|
||||
export const PLATFORM_TABLE_BODY_CLASS = 'divide-y divide-border';
|
||||
export const PLATFORM_TABLE_CLASS = 'platform-table';
|
||||
export const PLATFORM_TABLE_SUMMARY_ROW_CLASS = 'h-8';
|
||||
export const PLATFORM_TABLE_PHONE_IDENTITY_WIDTH_PERCENT = 30;
|
||||
export const PLATFORM_TABLE_NARROW_IDENTITY_WIDTH_PERCENT = 40;
|
||||
// Responsive platform tables already decide which columns remain useful at
|
||||
@@ -415,9 +416,10 @@ export const getPlatformTableHeadClassForKind = (kind: PlatformTableColumnKind):
|
||||
export const getPlatformTableCellClassForKind = (kind: PlatformTableColumnKind): string =>
|
||||
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 (
|
||||
|
||||
@@ -262,6 +262,7 @@ export function ProxmoxBackupServersTable(props: {
|
||||
<TableRow class="hover:bg-surface-hover">
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('name')} text-base-content truncate font-medium`}
|
||||
title={[row.serverName, row.datastore?.name].filter(Boolean).join(' · ')}
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<PlatformResourceDetailToggleButton
|
||||
@@ -276,19 +277,6 @@ export function ProxmoxBackupServersTable(props: {
|
||||
{row.serverName}
|
||||
</div>
|
||||
</div>
|
||||
<Show
|
||||
when={
|
||||
layoutMode() === 'compact' &&
|
||||
!columnVisible('datastore') &&
|
||||
row.datastore
|
||||
}
|
||||
>
|
||||
{(datastore) => (
|
||||
<div class="min-w-0 truncate font-mono text-[10px] font-normal text-muted">
|
||||
{datastore().name}
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
<div class="flex items-center gap-2">
|
||||
|
||||
@@ -426,7 +426,10 @@ export const ProxmoxCephTable: Component<{
|
||||
aria-expanded={isOpen()}
|
||||
tabIndex={0}
|
||||
>
|
||||
<TableCell class={getPlatformTableCellClassForKind('name')}>
|
||||
<TableCell
|
||||
class={getPlatformTableCellClassForKind('name')}
|
||||
title={[name, cluster.platformId].filter(Boolean).join(' · ')}
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<PlatformResourceDetailToggleButton
|
||||
expanded={isOpen()}
|
||||
@@ -438,16 +441,13 @@ export const ProxmoxCephTable: Component<{
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
<Show when={cluster.platformId}>
|
||||
<div
|
||||
class="hidden text-[10px] text-muted font-mono truncate md:block"
|
||||
title={cluster.platformId}
|
||||
>
|
||||
{cluster.platformId}
|
||||
</div>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
<TableCell
|
||||
class={getPlatformTableCellClassForKind('text')}
|
||||
title={[ind.label, cluster.ceph?.healthStatus]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<StatusDot
|
||||
size="sm"
|
||||
@@ -457,11 +457,6 @@ export const ProxmoxCephTable: Component<{
|
||||
/>
|
||||
<span class={`text-[11px] font-medium ${ind.tone}`}>{ind.label}</span>
|
||||
</div>
|
||||
<Show when={!!cluster.ceph?.healthStatus}>
|
||||
<div class="hidden text-[10px] text-muted font-mono md:block">
|
||||
{cluster.ceph?.healthStatus}
|
||||
</div>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<Show when={showFsid()}>
|
||||
<TableCell
|
||||
|
||||
@@ -358,7 +358,7 @@ export const ProxmoxNodesTable: Component<{
|
||||
</For>
|
||||
}
|
||||
body={
|
||||
<PlatformWindowedRows items={nodePreview.visibleRows} estimatedRowHeight={40}>
|
||||
<PlatformWindowedRows items={nodePreview.visibleRows} estimatedRowHeight={32}>
|
||||
{(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
|
||||
/>
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<div class="min-w-0 flex-1">
|
||||
<ResourceNameWithWebInterfaceLink
|
||||
name={name()}
|
||||
url={externalUrl()}
|
||||
class={`min-w-0 ${usesCondensedIdentity() ? '[&>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()}
|
||||
</ResourceNameWithWebInterfaceLink>
|
||||
<Show when={showNativeNodeName()}>
|
||||
<span class="-mt-0.5 truncate text-[10px] text-muted">
|
||||
{nativeNodeName()}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={!isOnline()}>
|
||||
<span class="hidden rounded bg-surface-alt px-1.5 py-0.5 text-[10px] font-medium text-muted sm:inline-flex">
|
||||
|
||||
@@ -116,15 +116,24 @@ export function ProxmoxRecoverableTable(props: {
|
||||
const renderRow = (artifact: RecoverableArtifact): JSX.Element => (
|
||||
<TableRow class="hover:bg-surface-hover" data-proxmox-backup-row="recoverable">
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('name')} text-base-content`}>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-semibold">
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<div
|
||||
class="min-w-0 truncate font-semibold"
|
||||
title={[
|
||||
artifact.workload.name || artifact.workload.label,
|
||||
artifact.workload.typeLabel,
|
||||
artifact.workload.vmid,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{artifact.workload.name || artifact.workload.label}
|
||||
</div>
|
||||
<Show when={isCompactBackupIdentityLayout(layoutMode())}>
|
||||
<div class="truncate text-[10px] text-muted">
|
||||
<span class="shrink-0 text-[10px] text-muted">
|
||||
{artifact.workload.typeLabel}
|
||||
<Show when={artifact.workload.vmid}> {artifact.workload.vmid}</Show>
|
||||
</div>
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -288,7 +288,7 @@ describe('ProxmoxNodesTable', () => {
|
||||
expect(link).toHaveAttribute('href', 'https://pve1:8006');
|
||||
});
|
||||
|
||||
it('keeps the provider-native node name visible beside a friendly name on phones', () => {
|
||||
it('keeps provider-native node identity without adding a phone subtitle', () => {
|
||||
render(() => (
|
||||
<ProxmoxNodesTable
|
||||
nodes={[
|
||||
@@ -313,7 +313,7 @@ describe('ProxmoxNodesTable', () => {
|
||||
));
|
||||
|
||||
expect(screen.getByText('West Production A')).toBeInTheDocument();
|
||||
expect(screen.getByText('pve1')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('West Production A · Proxmox node pve1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('passes alert-backed temperature thresholds into the node temperature gauge', () => {
|
||||
|
||||
@@ -4,7 +4,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { ProxmoxPageSurface } from '../ProxmoxPageSurface';
|
||||
import proxmoxPageSurfaceSource from '../ProxmoxPageSurface.tsx?raw';
|
||||
import proxmoxBackupServersTableSource from '../ProxmoxBackupServersTable.tsx?raw';
|
||||
import proxmoxBackupsTableSource from '../ProxmoxBackupsTable.tsx?raw';
|
||||
import proxmoxRecoverableTableSource from '../ProxmoxRecoverableTable.tsx?raw';
|
||||
|
||||
const mockUseUnifiedResources = vi.fn();
|
||||
const mockPathname = vi.hoisted(() => vi.fn(() => '/proxmox/overview'));
|
||||
@@ -351,6 +353,17 @@ describe('ProxmoxPageSurface contract', () => {
|
||||
expect(proxmoxBackupsTableSource).not.toContain('SavedViews');
|
||||
});
|
||||
|
||||
it('keeps backup summary identity on one canonical compact row', () => {
|
||||
expect(proxmoxBackupServersTableSource).not.toContain(
|
||||
'font-mono text-[10px] font-normal text-muted',
|
||||
);
|
||||
expect(proxmoxBackupServersTableSource).toContain(
|
||||
"title={[row.serverName, row.datastore?.name].filter(Boolean).join(' · ')}",
|
||||
);
|
||||
expect(proxmoxRecoverableTableSource).toContain('class="flex min-w-0 items-center gap-1"');
|
||||
expect(proxmoxRecoverableTableSource).not.toContain('class="truncate text-[10px] text-muted"');
|
||||
});
|
||||
|
||||
it('hydrates route-scoped resource families instead of one broad estate request', () => {
|
||||
setResources([
|
||||
makeResource({
|
||||
|
||||
@@ -363,7 +363,7 @@ const AgentMachineNetworkCell: Component<{
|
||||
<AgentMachineMetricTooltip
|
||||
triggerDataAttribute="data-agent-machine-network-trigger"
|
||||
tooltipDataAttribute="data-agent-machine-network-tooltip"
|
||||
triggerClass="grid w-full grid-cols-[0.75rem_minmax(0,1fr)] grid-rows-2 items-center gap-x-1 gap-y-0.5 text-[11px] leading-tight tabular-nums"
|
||||
triggerClass="flex w-full min-w-0 items-center gap-1 text-[11px] tabular-nums"
|
||||
tooltipClass="min-w-[230px] max-w-[360px] space-y-2"
|
||||
enabled={hasDetails()}
|
||||
ariaLabel={ariaLabel()}
|
||||
@@ -371,9 +371,9 @@ const AgentMachineNetworkCell: Component<{
|
||||
maxWidth={380}
|
||||
trigger={
|
||||
<>
|
||||
<span class="inline-flex w-3 justify-center text-emerald-500">↓</span>
|
||||
<span class="inline-flex shrink-0 text-emerald-500">↓</span>
|
||||
<span class="min-w-0 truncate">{formatSpeed(props.network?.rxBytes ?? 0)}</span>
|
||||
<span class="inline-flex w-3 justify-center text-orange-400">↑</span>
|
||||
<span class="inline-flex shrink-0 text-orange-400">↑</span>
|
||||
<span class="min-w-0 truncate">{formatSpeed(props.network?.txBytes ?? 0)}</span>
|
||||
</>
|
||||
}
|
||||
@@ -493,7 +493,7 @@ const AgentMachineDiskIOCell: Component<{
|
||||
<AgentMachineMetricTooltip
|
||||
triggerDataAttribute="data-agent-machine-diskio-trigger"
|
||||
tooltipDataAttribute="data-agent-machine-diskio-tooltip"
|
||||
triggerClass="grid w-full grid-cols-[0.75rem_minmax(0,1fr)] grid-rows-2 items-center gap-x-1 gap-y-0.5 text-[11px] leading-tight tabular-nums"
|
||||
triggerClass="flex w-full min-w-0 items-center gap-1 text-[11px] tabular-nums"
|
||||
tooltipClass="min-w-[230px] max-w-[360px] space-y-2"
|
||||
enabled={hasDetails()}
|
||||
ariaLabel={ariaLabel()}
|
||||
@@ -501,9 +501,9 @@ const AgentMachineDiskIOCell: Component<{
|
||||
maxWidth={380}
|
||||
trigger={
|
||||
<>
|
||||
<span class="inline-flex w-3 justify-center font-mono text-blue-500">R</span>
|
||||
<span class="inline-flex shrink-0 font-mono text-blue-500">R</span>
|
||||
<span class="min-w-0 truncate">{formatSpeed(props.diskIO?.readRate ?? 0)}</span>
|
||||
<span class="inline-flex w-3 justify-center font-mono text-amber-500">W</span>
|
||||
<span class="inline-flex shrink-0 font-mono text-amber-500">W</span>
|
||||
<span class="min-w-0 truncate">{formatSpeed(props.diskIO?.writeRate ?? 0)}</span>
|
||||
</>
|
||||
}
|
||||
@@ -1607,6 +1607,9 @@ export const AgentsMachinesTable: Component<{
|
||||
>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('name')} ${machineColumnWidthClass('machine')}`}
|
||||
title={[name(), machineSubtitle() || systemLabel()]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<PlatformResourceDetailToggleButton
|
||||
@@ -1628,26 +1631,6 @@ export const AgentsMachinesTable: Component<{
|
||||
nameClass="truncate font-semibold text-base-content"
|
||||
/>
|
||||
</div>
|
||||
<Show
|
||||
when={machineSubtitle()}
|
||||
fallback={
|
||||
<span
|
||||
class="mt-0.5 hidden truncate pl-10 text-[9px] text-muted sm:text-[10px] md:block"
|
||||
title={systemLabel()}
|
||||
>
|
||||
{systemLabel()}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(subtitle) => (
|
||||
<span
|
||||
class="mt-0.5 hidden truncate pl-10 text-[9px] text-muted sm:text-[10px] md:block"
|
||||
title={subtitle()}
|
||||
>
|
||||
{subtitle()}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</TableCell>
|
||||
<Show when={columnVisibility.isColumnVisible('system')}>
|
||||
<TableCell
|
||||
|
||||
@@ -325,21 +325,21 @@ describe('AgentsMachinesTable', () => {
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'Sort by IP' })).not.toBeInTheDocument();
|
||||
expect(screen.getByText('192.168.0.10')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Tower · 192.168.0.10')).toBeInTheDocument();
|
||||
await fireEvent.click(screen.getByLabelText('IP'));
|
||||
await fireEvent.click(screen.getByLabelText('RAID'));
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Sort by IP' })).toBeInTheDocument();
|
||||
// The primary IP remains in the compact identity subtitle as a fallback
|
||||
// while the dedicated user-selected column is container-hidden.
|
||||
expect(screen.getAllByText('192.168.0.10').length).toBeGreaterThanOrEqual(2);
|
||||
// The compact identity stays one line; the selected IP column owns the
|
||||
// visible value while the identity tooltip retains the same context.
|
||||
expect(screen.getAllByText('192.168.0.10').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('1 clean')).toBeInTheDocument();
|
||||
const memoryBar = screen.getByTestId('agent-machine-memory-bar');
|
||||
expect(memoryBar).toHaveAttribute('data-cache', '10');
|
||||
expect(memoryBar).not.toHaveAttribute('data-cache-inclusive-label', 'Shown in Proxmox');
|
||||
});
|
||||
|
||||
it('shows hostname and primary IP in the machine identity subtitle', () => {
|
||||
it('keeps hostname and primary IP in the single-line machine identity context', () => {
|
||||
render(() => (
|
||||
<AgentsMachinesTable
|
||||
resources={[
|
||||
@@ -355,7 +355,9 @@ describe('AgentsMachinesTable', () => {
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText('richard-mac-mini.local | 192.168.0.98')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTitle('Mac Mini · richard-mac-mini.local | 192.168.0.98'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('normalizes canonical OS labels from platform fallback values', () => {
|
||||
@@ -581,7 +583,7 @@ describe('AgentsMachinesTable', () => {
|
||||
expect(screen.getByText('Windows Runner')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('preserves last-seen context in the machine subtitle when that column is hidden', async () => {
|
||||
it('preserves last-seen context in the single-line machine identity when that column is hidden', async () => {
|
||||
vi.spyOn(Date, 'now').mockReturnValue(1_700_000_600_000);
|
||||
|
||||
render(() => (
|
||||
@@ -605,7 +607,7 @@ describe('AgentsMachinesTable', () => {
|
||||
await openMachineColumnPicker();
|
||||
await fireEvent.click(screen.getByLabelText('Last seen'));
|
||||
|
||||
expect(screen.getByText('192.168.0.21 | seen 5m ago')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Recent Machine · 192.168.0.21 | seen 5m ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows a row expansion affordance for machine details', async () => {
|
||||
|
||||
@@ -234,19 +234,21 @@ export const TrueNASAlertsTable: Component<{
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="truncate font-medium text-base-content"
|
||||
title={incident.resourceName}
|
||||
title={[
|
||||
incident.resourceName,
|
||||
formatPlatformAlertResourceType(
|
||||
incident.resourceType,
|
||||
'truenas',
|
||||
),
|
||||
incident.resource.parentName
|
||||
? `on ${incident.resource.parentName}`
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{incident.resourceName}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-muted">
|
||||
{formatPlatformAlertResourceType(
|
||||
incident.resourceType,
|
||||
'truenas',
|
||||
)}
|
||||
<Show when={incident.resource.parentName}>
|
||||
on {incident.resource.parentName}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -257,20 +259,25 @@ export const TrueNASAlertsTable: Component<{
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('text')}`}>
|
||||
<span class="block truncate text-base-content" title={incident.summary}>
|
||||
<span
|
||||
class="block truncate text-base-content"
|
||||
title={[incident.summary, incident.label].filter(Boolean).join(' · ')}
|
||||
>
|
||||
{incident.summary}
|
||||
</span>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{incident.label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class={`${getPlatformTableCellClassForKind('text')}`}>
|
||||
<span class="block truncate" title={incident.code}>
|
||||
<span
|
||||
class="block truncate"
|
||||
title={[
|
||||
formatPlatformAlertCode(incident.code, 'truenas'),
|
||||
incident.source,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{formatPlatformAlertCode(incident.code, 'truenas')}
|
||||
</span>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{incident.source}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
|
||||
|
||||
@@ -372,15 +372,17 @@ export const TrueNASAppsTable: Component<{
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="truenas-app-name-value truncate font-medium text-base-content"
|
||||
title={name()}
|
||||
title={[
|
||||
name(),
|
||||
`${formatPlatformTableTitleCaseValue(app()?.state)} on ${
|
||||
resource.parentName || 'TrueNAS'
|
||||
}${app()?.customApp ? ' · Custom' : ''}`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{name()}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-muted">
|
||||
{formatPlatformTableTitleCaseValue(app()?.state)} on{' '}
|
||||
{resource.parentName || 'TrueNAS'}
|
||||
<Show when={app()?.customApp}> - Custom</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -277,15 +277,20 @@ export const TrueNASNetworkSharesTable: Component<{
|
||||
title={indicator().label}
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-base-content" title={name()}>
|
||||
<div
|
||||
class="truncate font-medium text-base-content"
|
||||
title={[
|
||||
name(),
|
||||
share()?.dataset ||
|
||||
share()?.path ||
|
||||
resource.parentName ||
|
||||
'TrueNAS',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{name()}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-muted">
|
||||
{share()?.dataset ||
|
||||
share()?.path ||
|
||||
resource.parentName ||
|
||||
'TrueNAS'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -309,11 +314,6 @@ export const TrueNASNetworkSharesTable: Component<{
|
||||
title={[access().title, security().title].filter(Boolean).join(' | ')}
|
||||
>
|
||||
<span class="block truncate">{access().label}</span>
|
||||
<Show when={security().label !== '-'}>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{security().label}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
|
||||
|
||||
@@ -321,12 +321,12 @@ const DatasetCell: Component<{ point: RecoveryPoint; detailToggle?: JSX.Element
|
||||
title={getRecoveryOutcomeLabel(normalizeRecoveryOutcome(props.point.outcome))}
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-base-content" title={name()}>
|
||||
<div
|
||||
class="truncate font-medium text-base-content"
|
||||
title={[name(), subtitle()].filter(Boolean).join(' · ')}
|
||||
>
|
||||
{name()}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-muted" title={subtitle()}>
|
||||
{subtitle()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -533,30 +533,22 @@ export const TrueNASProtectionTable: Component<{
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
<span class="block truncate text-base-content" title={artifact()}>
|
||||
<span
|
||||
class="block truncate text-base-content"
|
||||
title={[artifact(), artifactSecondary()]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{artifact()}
|
||||
</span>
|
||||
<Show when={artifactSecondary()}>
|
||||
<span
|
||||
class="block truncate text-[10px] text-muted"
|
||||
title={artifactSecondary()}
|
||||
>
|
||||
{artifactSecondary()}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
<span class="block truncate text-base-content" title={target()}>
|
||||
<span
|
||||
class="block truncate text-base-content"
|
||||
title={[target(), targetSecondary()].filter(Boolean).join(' · ')}
|
||||
>
|
||||
{target()}
|
||||
</span>
|
||||
<Show when={targetSecondary()}>
|
||||
<span
|
||||
class="block truncate text-[10px] text-muted"
|
||||
title={targetSecondary()}
|
||||
>
|
||||
{targetSecondary()}
|
||||
</span>
|
||||
</Show>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
|
||||
|
||||
@@ -265,12 +265,12 @@ const ResourceCell: Component<{ row: TrueNASStorageTopologyRow; detailToggle?: J
|
||||
{props.detailToggle}
|
||||
<StatusDot size="sm" variant={indicator().variant} title={indicator().label} />
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-base-content" title={name()}>
|
||||
<div
|
||||
class="truncate font-medium text-base-content"
|
||||
title={[name(), subtitle(props.row)].filter(Boolean).join(' · ')}
|
||||
>
|
||||
{name()}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-muted" title={subtitle(props.row)}>
|
||||
{subtitle(props.row)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -380,15 +380,10 @@ export const TrueNASSystemsTable: Component<{
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="truncate font-semibold text-base-content"
|
||||
title={name()}
|
||||
title={[name(), systemMeta()].filter(Boolean).join(' · ')}
|
||||
>
|
||||
{name()}
|
||||
</div>
|
||||
<Show when={systemMeta()}>
|
||||
<div class="truncate text-[11px] text-muted" title={systemMeta()}>
|
||||
{systemMeta()}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -443,14 +438,11 @@ export const TrueNASSystemsTable: Component<{
|
||||
<PlatformTableTemperatureValue value={system.temperature} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden whitespace-normal text-base-content lg:table-cell`}
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
|
||||
title={`${storageInventoryPrimary(c())} · ${storageInventorySecondary(c())}`}
|
||||
>
|
||||
<div class="leading-tight">
|
||||
<div class="truncate">{storageInventoryPrimary(c())}</div>
|
||||
<div class="truncate text-[11px] text-muted">
|
||||
{storageInventorySecondary(c())}
|
||||
</div>
|
||||
<div class="truncate">
|
||||
{storageInventoryPrimary(c())} · {storageInventorySecondary(c())}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
@@ -459,14 +451,11 @@ export const TrueNASSystemsTable: Component<{
|
||||
<PlatformTableNumberValue value={c().shares} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden whitespace-normal text-base-content lg:table-cell`}
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
|
||||
title={`${workloadInventoryPrimary(c())} · ${workloadInventorySecondary(c())}`}
|
||||
>
|
||||
<div class="leading-tight">
|
||||
<div class="truncate">{workloadInventoryPrimary(c())}</div>
|
||||
<div class="truncate text-[11px] text-muted">
|
||||
{workloadInventorySecondary(c())}
|
||||
</div>
|
||||
<div class="truncate">
|
||||
{workloadInventoryPrimary(c())} · {workloadInventorySecondary(c())}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
|
||||
@@ -310,15 +310,20 @@ export const TrueNASVirtualMachinesTable: Component<{
|
||||
title={indicator().label}
|
||||
/>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate font-medium text-base-content" title={name()}>
|
||||
<div
|
||||
class="truncate font-medium text-base-content"
|
||||
title={[
|
||||
name(),
|
||||
vm()?.description ||
|
||||
vm()?.uuid ||
|
||||
resource.parentName ||
|
||||
'TrueNAS',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{name()}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-muted">
|
||||
{vm()?.description ||
|
||||
vm()?.uuid ||
|
||||
resource.parentName ||
|
||||
'TrueNAS'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('TrueNASAlertsTable', () => {
|
||||
detail.getByText('Investigate disk health and schedule replacement if degradation continues'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(detail.getByRole('button', { name: 'Close' }));
|
||||
await fireEvent.click(detail.getByRole('button', { name: /^Collapse .* details$/ }));
|
||||
|
||||
expect(screen.queryByTestId('truenas-alert-detail')).not.toBeInTheDocument();
|
||||
expect(row).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
@@ -116,7 +116,7 @@ describe('TrueNASProtectionTable', () => {
|
||||
expect(detail.getAllByText('vault/compliance/tank_apps').length).toBeGreaterThan(1);
|
||||
expect(detail.getByText('tank/apps@auto-20260331-0600')).toBeInTheDocument();
|
||||
|
||||
await fireEvent.click(detail.getByRole('button', { name: 'Close' }));
|
||||
await fireEvent.click(detail.getByRole('button', { name: /^Collapse .* details$/ }));
|
||||
|
||||
expect(screen.queryByTestId('truenas-protection-detail')).not.toBeInTheDocument();
|
||||
expect(row).toHaveAttribute('aria-expanded', 'false');
|
||||
|
||||
@@ -400,16 +400,18 @@ export const VsphereActivityTable: Component<{
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="truncate font-medium text-base-content"
|
||||
title={activity.resourceName}
|
||||
title={[
|
||||
activity.resourceName,
|
||||
formatResourceType(activity.resourceType),
|
||||
activity.resource.parentName
|
||||
? `on ${activity.resource.parentName}`
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{activity.resourceName}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-muted">
|
||||
{formatResourceType(activity.resourceType)}
|
||||
<Show when={activity.resource.parentName}>
|
||||
on {activity.resource.parentName}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -419,12 +421,17 @@ export const VsphereActivityTable: Component<{
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
<span class="block truncate text-base-content" title={activity.title}>
|
||||
<span
|
||||
class="block truncate text-base-content"
|
||||
title={[
|
||||
activity.title,
|
||||
activity.message || activity.description || activity.nativeId,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{activity.title}
|
||||
</span>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{activity.message || activity.description || activity.nativeId}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('badge')}>
|
||||
<span class="text-[11px] font-semibold text-base-content">
|
||||
@@ -443,13 +450,15 @@ export const VsphereActivityTable: Component<{
|
||||
>
|
||||
<span
|
||||
class="block truncate text-base-content"
|
||||
title={meta()?.connectionName || meta()?.vcenterHost}
|
||||
title={[
|
||||
meta()?.connectionName || meta()?.vcenterHost,
|
||||
meta()?.datacenterName || meta()?.clusterName,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{meta()?.connectionName || meta()?.vcenterHost || '-'}
|
||||
</span>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{meta()?.datacenterName || meta()?.clusterName || '-'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
|
||||
|
||||
@@ -240,16 +240,21 @@ export const VsphereAlertsTable: Component<{
|
||||
<div class="min-w-0">
|
||||
<div
|
||||
class="truncate font-medium text-base-content"
|
||||
title={incident.resourceName}
|
||||
title={[
|
||||
incident.resourceName,
|
||||
formatPlatformAlertResourceType(
|
||||
incident.resourceType,
|
||||
'vmware',
|
||||
),
|
||||
incident.resource.parentName
|
||||
? `on ${incident.resource.parentName}`
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{incident.resourceName}
|
||||
</div>
|
||||
<div class="truncate text-[10px] text-muted">
|
||||
{formatPlatformAlertResourceType(incident.resourceType, 'vmware')}
|
||||
<Show when={incident.resource.parentName}>
|
||||
on {incident.resource.parentName}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -260,33 +265,40 @@ export const VsphereAlertsTable: Component<{
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
<span class="block truncate text-base-content" title={incident.summary}>
|
||||
<span
|
||||
class="block truncate text-base-content"
|
||||
title={[incident.summary, incident.label].filter(Boolean).join(' · ')}
|
||||
>
|
||||
{incident.summary}
|
||||
</span>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{incident.label}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell class={getPlatformTableCellClassForKind('text')}>
|
||||
<span
|
||||
class="block truncate text-base-content"
|
||||
title={meta()?.connectionName || meta()?.vcenterHost}
|
||||
title={[
|
||||
meta()?.connectionName || meta()?.vcenterHost,
|
||||
meta()?.datacenterName || meta()?.clusterName,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{meta()?.connectionName || meta()?.vcenterHost || '-'}
|
||||
</span>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{meta()?.datacenterName || meta()?.clusterName || '-'}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('text')} hidden text-base-content lg:table-cell`}
|
||||
>
|
||||
<span class="block truncate" title={incident.managedObjectId}>
|
||||
<span
|
||||
class="block truncate"
|
||||
title={[
|
||||
formatPlatformAlertEntityType(incident.entityType),
|
||||
incident.managedObjectId,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')}
|
||||
>
|
||||
{formatPlatformAlertEntityType(incident.entityType)}
|
||||
</span>
|
||||
<span class="block truncate text-[10px] text-muted">
|
||||
{incident.managedObjectId}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
class={`${getPlatformTableCellClassForKind('numeric-value')} text-base-content`}
|
||||
|
||||
@@ -99,7 +99,9 @@ describe('VsphereActivityTable', () => {
|
||||
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');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user