From ff2bfbb320cc2298068c826990a13f9a87dcb003 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 14 Aug 2026 00:18:23 +0100 Subject: [PATCH] Make platform telemetry presentation evidence-aware --- .../v6/internal/subsystems/agent-lifecycle.md | 11 ++- .../v6/internal/subsystems/api-contracts.md | 20 +++-- .../subsystems/frontend-primitives.md | 7 ++ .../subsystems/performance-and-scalability.md | 40 +++++++-- .../internal/subsystems/storage-recovery.md | 5 +- .../internal/subsystems/unified-resources.md | 6 ++ frontend-modern/browser-verification.json | 66 ++++++++++---- .../__tests__/runtimeInventorySources.test.ts | 10 +++ .../src/api/runtimeInventorySources.ts | 15 +++- .../Infrastructure/ResourceDetailDrawer.tsx | 2 +- ...urceDetailDrawer.docker-container.test.tsx | 4 +- .../ResourceDetailDrawer.history.test.tsx | 7 ++ ...ourceDetailDrawer.machine-history.test.tsx | 6 +- ...rMetricsHistoryModel.branchcov0712.test.ts | 30 +++---- ...resourceDetailDrawerMetricsHistoryModel.ts | 2 +- .../useResourceDetailDrawerDerivedState.ts | 8 +- .../components/Workloads/GuestDrawer.test.tsx | 34 +++++++ .../src/components/Workloads/GuestDrawer.tsx | 10 +-- .../Workloads/GuestDrawerHistory.tsx | 47 +++------- .../Workloads/GuestDrawerOverview.tsx | 11 ++- .../src/components/Workloads/GuestRow.tsx | 63 +++++++------ .../src/components/Workloads/NodeDrawer.tsx | 6 +- .../Workloads/__tests__/GuestRow.test.tsx | 54 ++++++++++++ ...loadsSurface.performance.contract.test.tsx | 7 +- .../guestDrawerModel.branchcov2.test.ts | 22 +++-- .../nodeDrawerModel.branchcov0712c.test.ts | 26 +++--- .../nodeDrawerModel.branchcov0713.test.ts | 23 +++-- .../nodeDrawerModel.branchcov2.test.ts | 27 +++--- .../components/Workloads/guestDrawerModel.ts | 47 ++++++---- .../components/Workloads/nodeDrawerModel.ts | 4 +- .../Workloads/useGuestDrawerState.ts | 3 + .../components/Workloads/useWorkloadsState.ts | 17 +++- .../workloadInventorySourceIssues.ts | 1 + .../src/features/docker/DockerHostDrawer.tsx | 6 +- .../__tests__/dockerHostDrawerModel.test.ts | 34 ++++--- .../features/docker/dockerHostDrawerModel.ts | 2 +- .../src/features/vmware/VmwarePageSurface.tsx | 88 ++++++++++++++++++- .../VmwarePageSurface.contract.test.tsx | 41 +++++++++ .../src/hooks/__tests__/useWorkloads.test.ts | 85 ++++++++++++++++++ frontend-modern/src/hooks/useWorkloads.ts | 84 +++++++++++++----- frontend-modern/src/types/workloads.ts | 17 ++++ internal/api/connections_aggregator.go | 32 +++++++ internal/api/connections_aggregator_test.go | 36 ++++++++ internal/api/connections_types.go | 1 + internal/api/runtime_inventory_sources.go | 40 ++++++--- .../api/runtime_inventory_sources_test.go | 54 ++++++++++++ 46 files changed, 891 insertions(+), 270 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index cd2422756..ef21a6af0 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -6211,14 +6211,17 @@ payload and authorization proof. The authenticated `GET /api/runtime/inventory-sources` monitoring projection derives cached source health through the shared connection aggregator but is not -an agent enumeration, enrollment, update, or command route. Its four-field wire -type carries only source `type`, operator-facing `name`, normalized blocking -`state`, and workload-only `surfaces`. +an agent enumeration, enrollment, update, or command route. Its base wire type +carries only source `type`, operator-facing `name`, normalized lifecycle +`state`, and workload-only `surfaces`. An optional VMware completeness summary +may carry only degraded state, aggregate count, and bounded stage/category/ +occurrence tuples for otherwise-successful inventory reads. It has no stable connection or agent ID, token binding, address, report IP, hostname identity object, operating-system, kernel or architecture fact, agent version, update or module state, command-session state, commands-enabled value, -fleet policy, or credential-health object. The handler deliberately skips the +fleet policy, credential-health object, raw enrichment error, or affected +entity identity. The handler deliberately skips the command-session enrichment used by the administrative ledger. Agent reporting, registration, update, and command authority remain unchanged, and the fully-populated projection test fails if any of those lifecycle facts cross the diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 115450263..6438f61d6 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -9309,10 +9309,13 @@ same narrow response. `GET /api/connections` remains unchanged behind `RequireAdmin` plus `settings:read` and continues to be the only connection configuration and fleet ledger. -The response envelope is `{"sources": [...]}` and every source has exactly four -fields: operator-facing `type` and `name`, normalized blocking `state`, and a -`surfaces` array containing only effective workload coverage. The backend drops -healthy, disabled, non-workload, and coverage-free connections before the wire; +The response envelope is `{"sources": [...]}` and every source has four base +fields: operator-facing `type` and `name`, normalized lifecycle `state`, and a +`surfaces` array containing only effective workload coverage. A VMware source +whose otherwise-successful poll reported optional enrichment failures may add +`completeness={state:"degraded",issueCount,issues}`; each issue is limited to +stage, category, and occurrence count. The backend drops healthy sources with +no completeness issue, disabled, non-workload, and coverage-free connections before the wire; resolves configured scope over declared surfaces; removes storage and backup coverage; and normalizes cached invalid or expired credential health to `state="unauthorized"`. There is no privileged variant. @@ -9320,9 +9323,12 @@ coverage; and normalizes cached invalid or expired credential health to The whitelist intentionally has no connection ID, address, host alias, raw state reason or error, timestamp, source origin, agent identity, version or module, fleet policy or credential-health object, capability, credential, -configuration, or mutation field. -`TestRuntimeInventorySourceWireShapeIsAnExactWhitelist` fails if the Go wire -type grows, while +configuration, raw enrichment message, affected entity identity, or mutation +field. `RuntimeInventoryCompleteness` is an explicit second whitelist rather +than a serialized VMware observed summary. +`TestRuntimeInventorySourceWireShapeIsAnExactWhitelist` fails if the base Go +wire type grows, while the completeness projection test proves raw messages, +entity IDs, and endpoints cannot cross the optional diagnostic shape, and `TestRuntimeInventorySourcesOmitAdministrativeAndSensitiveFacts` projects a fully populated administrative record and proves those values cannot travel. The route-authorization test proves the exact viewer 200 / ledger 403 pairing, diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index e845dccf7..4c27c9421 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -47,6 +47,13 @@ the workload state, table, panel, and row contracts carry the selected basis and resolved parent-node data to the canonical memory bar, and the memory column header must expose the non-default Host basis after the control closes. +Shared workload, node, Docker-host, and resource-drawer history presentation +keeps current readings separate from stored samples. A current metric may +populate the legend while history is still being collected, but it must never +be expanded into synthetic timestamps or chart geometry. An empty stored +series renders the shared collecting-history state; zero remains a valid +reported reading, while an absent metric remains unavailable. + Presentation helpers that mirror a server-side classification must name the predicate they mirror and expose it as a single exported function rather than diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 00218ab63..788f2d7d0 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -273,6 +273,13 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. `docker.uptimeSeconds`, `kubernetes.uptimeSeconds`); platforms whose adapters only populate the canonical field — vSphere is the working example — would otherwise render blank uptime cells for every row. + That mapper must preserve observation presence separately from the numeric + fallback required by the legacy `VM | Container` shape. `0` is a real + reported CPU, capacity, I/O, or uptime value; an absent or non-finite API + field is unavailable and must render as such. The mapper records that fact + once on `WorkloadGuest.telemetryAvailability`, never substitutes wall-clock + time for an absent `lastSeen`, and reads vSphere CPU allocation from + `VMware.CPUCount` before declaring the count unknown. Workload guest rows may compose the shared saved web-interface adjacent launch control from `frontend-primitives`, but row rendering must receive any saved URL from already-loaded row metadata and must not add per-row @@ -500,8 +507,8 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. scope must be applied before deriving host, Kubernetes context/namespace, and container runtime facet options so embedded pages do not pay for or display unrelated platform options. - Workloads source-health messaging must derive from the canonical - `/api/connections` ledger through + Workloads source-health messaging must derive from the viewer-safe + `/api/runtime/inventory-sources` projection through `frontend-modern/src/components/Workloads/workloadInventorySourceIssues.ts`. Platform-owned embedded Workloads surfaces must scope those source-health issues to the forced platform before rendering an empty inventory state, and @@ -530,6 +537,14 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. pending, or paused, but it must not fabricate VM/container rows from host telemetry or hide source blockers just because another source still has workload rows. + The same projection may include an active VMware source only when its last + otherwise-successful refresh reported degraded optional enrichment. That + completeness record is a strict whitelist of stage, category, occurrence, + and aggregate count; raw messages, entity identifiers, endpoints, and + credentials remain private. The vSphere page shares this single bounded + poll with its embedded Workloads state and must say when displayed inventory + may be incomplete rather than treating omitted enrichment as a healthy + empty result. The retired dashboard overview route must not return as a hot-path orientation shortcut. First-viewport system count, health, source coverage, and freshness now belong to the provider-first platform landing surface and @@ -571,6 +586,11 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. the migration boundary. 11. Extend workload drawer derivations and runtime wiring through `frontend-modern/src/components/Workloads/guestDrawerModel.ts` and `frontend-modern/src/components/Workloads/useGuestDrawerState.ts`, and extend drawer overview rendering through `frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx`, rather than rebuilding canonical guest identity, discovery routing, or drawer-local normalization inside `frontend-modern/src/components/Workloads/GuestDrawer.tsx` Drawer history charts belong to `frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx`. + A current metric may remain visible in a chart legend while history is + accumulating, but it must never be expanded into synthetic timestamps or a + flat line. Fewer than two stored points remains `Collecting history` and + only metrics-store samples may contribute chart geometry, bounds, or hover + inspection. History cards must let the plot area stretch to the card height instead of pinning the SVG wrapper to a fixed short height, so guest and node drawer history cards do not leave unused card space when headers have different @@ -1137,8 +1157,9 @@ node-context metric, not a universal Workloads table column, and the node detail drawer may add one compact `Thermals` history group beside utilization and I/O only after a grouped node row is selected. Host-agent CPU temperature must be persisted into the same metrics-store history stream as other agent metrics, -with current node temperature used only as drawer-local fallback while history -is still accumulating. +with current node temperature displayed only as a drawer-local current reading +while history is still accumulating; it must not become a synthetic history +point. Standalone agent thermal pressure follows a compact table-and-drawer rule: platform tables may show a short pressure status such as `Nominal` or `Constrained` in the existing temperature cell when no positive Celsius reading @@ -2381,10 +2402,13 @@ interval refetch. The server work remains one cached `buildConnections` aggregation with no probing, persistence, grouped-system construction, or command-session -enrichment. It filters healthy, disabled, non-workload, and coverage-free rows -before serialization, so the response is no larger than the actionable banner -input. Client presentation is a single pass over that bounded list and performs -no per-source fetch or detail lookup. +enrichment. It filters disabled, coverage-free, and healthy sources with no +collection-completeness issue before serialization, so the response is no +larger than actionable blocker or degraded-completeness input. Client +presentation is a single pass over that bounded list and performs no per-source +fetch or detail lookup. Platform pages that need the same snapshot must inject +the owning query into embedded Workloads state so they do not create a second +poll. ### Resource policy reads stay outside the Workloads hot path diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 240afe69f..ca4bb21bf 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -5173,7 +5173,10 @@ The projection filters coverage to workload labels (`vms`, containers, pods, and Kubernetes) before serialization. Storage and backup labels, source addresses, stable connection IDs, raw error text, credential state objects, and all administrative configuration remain absent by construction. The -api-contracts contract owns the authoritative four-field payload and +optional VMware completeness summary may identify degraded inventory stages by +category and count, but it carries no raw message and is not backup +completeness, protection damage, snapshot failure, restore readiness, or +recovery evidence. The api-contracts contract owns the authoritative payload and authorization proof. ### RBAC identity metadata and deprovisioning preserve storage boundaries diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index f1a53eed2..b651597c8 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -63,6 +63,12 @@ metric object exists, so `/api/resources`, `/api/state`, websocket resources, history, alerts, and UI adapters never need to interpret `null` as either idle or unknown. Unified resources must not infer validity from the numeric guest compatibility field alone. +Resource and workload drawer consumers preserve the same evidence boundary: +current CPU, memory, disk, network, and disk-I/O observations may be shown as +current values, but only samples returned by the canonical history endpoint may +form a historical series. Drawer adapters must not manufacture prior samples +from a current snapshot, and must retain explicit metric absence independently +from a valid reported zero. Proxmox node inventory continuity is monitoring-authored and unified-resource-consumed. A powered-off or temporarily unreachable cluster member must remain one canonical `agent` resource while monitoring still diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index f19fe4384..d962957d1 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,18 +1,52 @@ { "version": 1, - "base_sha": "a7646e5f86098b014476669fdc8105a692c22780", - "verified_at": "2026-08-13T22:56:10Z", + "base_sha": "903b579f8aa24e90ee00c4cac6f7aa8393f69584", + "verified_at": "2026-08-13T23:13:56Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Alerts/AlertResourceTableMobile.tsx", - "frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts" + "frontend-modern/src/api/runtimeInventorySources.ts", + "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx", + "frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts", + "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts", + "frontend-modern/src/components/Workloads/GuestDrawer.tsx", + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx", + "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx", + "frontend-modern/src/components/Workloads/GuestRow.tsx", + "frontend-modern/src/components/Workloads/NodeDrawer.tsx", + "frontend-modern/src/components/Workloads/guestDrawerModel.ts", + "frontend-modern/src/components/Workloads/nodeDrawerModel.ts", + "frontend-modern/src/components/Workloads/useGuestDrawerState.ts", + "frontend-modern/src/components/Workloads/useWorkloadsState.ts", + "frontend-modern/src/components/Workloads/workloadInventorySourceIssues.ts", + "frontend-modern/src/features/docker/DockerHostDrawer.tsx", + "frontend-modern/src/features/docker/dockerHostDrawerModel.ts", + "frontend-modern/src/features/vmware/VmwarePageSurface.tsx", + "frontend-modern/src/hooks/useWorkloads.ts", + "frontend-modern/src/types/workloads.ts" ], "content_sha256": { - "frontend-modern/src/components/Alerts/AlertResourceTableMobile.tsx": "6596f8b71c9cefb759d72751e4c5eeaf48292cd7fd90b3cc3a3f2026f59baf82", - "frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts": "6db0718b9139daa8f98a41dc0d254bf3889b138ded7b3ac9a2ffa761ab3de70e" + "frontend-modern/src/api/runtimeInventorySources.ts": "c14c416bc12ff77f53aa7623a774e00129e813771e9e8bbcd295ca5698615f2c", + "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx": "effbe9e1a206e6fc4aeb8d0f4f45c4ebe15d983fc6e98db5285f9cf623ea654b", + "frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts": "b21cbb340be34ee833503eab8bcdbc8a75e14e9bbc8bd6e0be8c1d8aa5d26cf1", + "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts": "4ae7d3a10b0850edd6193ccd2002275fb623deed8948c0dbbb2cd3055381e927", + "frontend-modern/src/components/Workloads/GuestDrawer.tsx": "f6de7da0928440d8edaacd34adc64de70577c88056cdaf88b409a91cc1954663", + "frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx": "a4f0c6e11a51b37f6f8fc5602ae7fb22f58b4217426c5cb1cf57e49936a92073", + "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx": "30e2a4f60244a0a07017562bef58d1d735f03a7ddba72f49586ccbf86387a4fb", + "frontend-modern/src/components/Workloads/GuestRow.tsx": "8db3148e7711f6bcdce117c4c58b75ebf214717056a034273a39d90475cac337", + "frontend-modern/src/components/Workloads/NodeDrawer.tsx": "9bdb7203eb698b7705914357b45245f17252aa135ef5b94eec331396fc04ec82", + "frontend-modern/src/components/Workloads/guestDrawerModel.ts": "18058d98f8909893e4b2e62c7aae569ed1e57c1dd8432f7a1efeaf6edc8c270f", + "frontend-modern/src/components/Workloads/nodeDrawerModel.ts": "5ba1927c2f050b26d67a6998f34cabbddba9fa79df98ec1e2ecc9b311633eeaa", + "frontend-modern/src/components/Workloads/useGuestDrawerState.ts": "dc0578bd3ea010412673a821291e6bd77a5fb229cb67e3493d4c4842af5e0345", + "frontend-modern/src/components/Workloads/useWorkloadsState.ts": "487eed98aba85f2dad8f504694ebf24c8faff861eeb19d729c741cb5cc3367b5", + "frontend-modern/src/components/Workloads/workloadInventorySourceIssues.ts": "abe71a344a7ff660e62b1cde6cafac72567e485827c08481003567ed3e7c3363", + "frontend-modern/src/features/docker/DockerHostDrawer.tsx": "e1d36317c1cefa262cfec544e8220608d53dd2fb546d8e058fa26caa5c0012d5", + "frontend-modern/src/features/docker/dockerHostDrawerModel.ts": "7162841abb5e5061cf89342d265c8e170243ddf41477985da350e85126eea594", + "frontend-modern/src/features/vmware/VmwarePageSurface.tsx": "2e44e78339c0bbd17dc1d2d75a8beef934393d79f72a105b54a25d3a0cbe84cb", + "frontend-modern/src/hooks/useWorkloads.ts": "856b6e05e39f0188b371878cc5361fe72f4a05b77b8505b00e0f19d3fedcfd46", + "frontend-modern/src/types/workloads.ts": "d3d5958364ebb71740ceee7fb804dd54594297d0a1f173556450772fcefc252c" }, "routes": [ - "/alerts/thresholds" + "/vmware/overview" ], "viewports": [ { @@ -25,16 +59,16 @@ } ], "states": [ - "VMs & Containers section expanded in the card layout at 700px with Backup and Snapshot rendered as toggle badges (Disabled) instead of number inputs", - "guest card edit mode (cold-archive-01) showing numeric inputs with On toggles for CPU/Memory/Disk while Backup and Snapshot remain toggle badges with no number input", - "unsaved-changes banner after toggling a backup badge, then discarded", - "1280px desktop viewport still using the card layout for the 14-column guests section, badges rendering identically" + "desktop vSphere host and workload tables without unsupported backup status columns or failure icons, with API-backed VM CPU allocation values visible", + "etl-batch-01 guest drawer overview showing the explicitly identified Pulse Agent presentation", + "etl-batch-01 history tab showing current readings alongside Collecting history with no fabricated chart geometry", + "700px compact vSphere tables with no horizontal body overflow or unsupported backup artifacts" ], "interactions": [ - "expanded the VMs & Containers group", - "entered per-guest edit mode via Edit thresholds button", - "clicked a Backup toggle badge in card view mode and confirmed it flipped to enabled and staged unsaved changes", - "clicked Discard to clear the staged change", - "checked console errors (only pre-existing WebSocket reconnect noise from the absent 7655 backend)" + "expanded etl-batch-01 and opened its guest drawer", + "switched the guest drawer to History", + "inspected all three history chart groups and confirmed zero SVG trend paths while no stored samples were available", + "verified the desktop and 700px layouts had no console errors", + "verified the 700px document scroll width matched the viewport width" ] } diff --git a/frontend-modern/src/api/__tests__/runtimeInventorySources.test.ts b/frontend-modern/src/api/__tests__/runtimeInventorySources.test.ts index 2b814e9c9..cdc160f85 100644 --- a/frontend-modern/src/api/__tests__/runtimeInventorySources.test.ts +++ b/frontend-modern/src/api/__tests__/runtimeInventorySources.test.ts @@ -21,6 +21,11 @@ describe('RuntimeInventorySourcesAPI', () => { name: 'Primary vCenter', state: 'unreachable', surfaces: ['vms'], + completeness: { + state: 'degraded', + issueCount: 2, + issues: [{ stage: 'tags', category: 'permission', occurrences: 2 }], + }, }, ], }); @@ -32,6 +37,11 @@ describe('RuntimeInventorySourcesAPI', () => { name: 'Primary vCenter', state: 'unreachable', surfaces: ['vms'], + completeness: { + state: 'degraded', + issueCount: 2, + issues: [{ stage: 'tags', category: 'permission', occurrences: 2 }], + }, }, ], }); diff --git a/frontend-modern/src/api/runtimeInventorySources.ts b/frontend-modern/src/api/runtimeInventorySources.ts index e7d074f9b..0b5499c24 100644 --- a/frontend-modern/src/api/runtimeInventorySources.ts +++ b/frontend-modern/src/api/runtimeInventorySources.ts @@ -3,7 +3,19 @@ import { apiFetchJSON } from '@/utils/apiClient'; export type RuntimeInventorySourceType = 'pve' | 'vmware' | 'docker' | 'kubernetes'; export type RuntimeInventorySourceState = - 'paused' | 'pending' | 'stale' | 'unauthorized' | 'unreachable'; + 'active' | 'paused' | 'pending' | 'stale' | 'unauthorized' | 'unreachable'; + +export interface RuntimeInventoryCompletenessIssue { + stage?: string; + category?: string; + occurrences?: number; +} + +export interface RuntimeInventoryCompleteness { + state: 'degraded'; + issueCount: number; + issues?: RuntimeInventoryCompletenessIssue[]; +} /** * Complete viewer-safe wire shape returned by GET /api/runtime/inventory-sources. @@ -14,6 +26,7 @@ export interface RuntimeInventorySource { name: string; state: RuntimeInventorySourceState; surfaces: string[]; + completeness?: RuntimeInventoryCompleteness; } export interface RuntimeInventorySourcesResponse { diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx index 79e5d9246..c81e8cd56 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx @@ -197,7 +197,7 @@ const DrawerContent: Component = (props) => { diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.docker-container.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.docker-container.test.tsx index 7089cb188..956060d6a 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.docker-container.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.docker-container.test.tsx @@ -8,14 +8,14 @@ vi.mock('@/components/Workloads/GuestDrawerHistory', () => ({ GuestDrawerHistory: (props: { target: { resourceType: string; resourceId: string } | null; range: string; - fallbackMetrics?: Record; + currentMetrics?: Record; }) => (
), GuestDrawerHistoryRangeSelect: (props: { diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx index 86c236c62..679de240f 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx @@ -20,6 +20,7 @@ import resourceDetailDrawerVmwareModelSource from '@/components/Infrastructure/r import resourceDetailDrawerTrueNASModelSource from '@/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts?raw'; import resourceDetailDrawerDockerActionsStateSource from '@/components/Infrastructure/useResourceDetailDrawerDockerActionsState.ts?raw'; import resourceDetailDrawerStateSource from '@/components/Infrastructure/useResourceDetailDrawerState.ts?raw'; +import guestDrawerHistorySource from '@/components/Workloads/GuestDrawerHistory.tsx?raw'; import actionAuditApiSource from '@/api/actionAudit.ts?raw'; import actionAuditPresentationSource from '@/utils/actionAuditPresentation.ts?raw'; import type { Resource } from '@/types/resource'; @@ -139,6 +140,12 @@ const baseResource = (overrides: Partial): Resource => ({ }); describe('ResourceDetailDrawer change history section', () => { + it('keeps current readings separate from stored history samples', () => { + expect(guestDrawerHistorySource).toContain('currentMetrics'); + expect(guestDrawerHistorySource).toContain('Collecting history'); + expect(guestDrawerHistorySource).not.toContain('buildFallbackHistoryPoints'); + }); + it('keeps discovery context presentation separate from discovery runtime ownership', () => { expect(discoveryTabSource).toContain('useDiscoveryTabState'); expect(discoveryTabStateSource).toContain('export function useDiscoveryTabState'); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.machine-history.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.machine-history.test.tsx index 0bad6ab8b..8d2935349 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.machine-history.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.machine-history.test.tsx @@ -7,15 +7,15 @@ vi.mock('@/components/Workloads/GuestDrawerHistory', () => ({ GuestDrawerHistory: (props: { target: { resourceType: string; resourceId: string } | null; range: string; - fallbackMetrics?: Record; + currentMetrics?: Record; }) => (
), GuestDrawerHistoryRangeSelect: (props: { diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.branchcov0712.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.branchcov0712.test.ts index 48841fc9a..1c5514489 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.branchcov0712.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerMetricsHistoryModel.branchcov0712.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { - getResourceMetricsHistoryFallbackMetrics, + getResourceMetricsHistoryCurrentMetrics, getResourceMetricsHistoryGroups, getResourceMetricsHistoryTarget, } from '@/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel'; @@ -109,7 +109,7 @@ describe('getResourceMetricsHistoryTarget branch coverage', () => { }); }); -describe('getResourceMetricsHistoryFallbackMetrics branch coverage', () => { +describe('getResourceMetricsHistoryCurrentMetrics branch coverage', () => { it('returns every metric as a finite number when the resource is fully populated', () => { // All finiteMetric calls take the "finite number -> return value" arm; // memory/disk ternaries take the TRUE arm and getMemoryPercent/getDiskPercent @@ -121,7 +121,7 @@ describe('getResourceMetricsHistoryFallbackMetrics branch coverage', () => { network: { rxBytes: 1000, txBytes: 2000 }, diskIO: { readRate: 3000, writeRate: 4000 }, }); - expect(getResourceMetricsHistoryFallbackMetrics(resource)).toStrictEqual({ + expect(getResourceMetricsHistoryCurrentMetrics(resource)).toStrictEqual({ cpu: 12.5, memory: 30, // (30 / 100) * 100 disk: 40, // (80 / 200) * 100 @@ -140,7 +140,7 @@ describe('getResourceMetricsHistoryFallbackMetrics branch coverage', () => { // Every optional chain short-circuits to undefined; memory/disk ternaries // take the FALSE arm because `resource.memory`/`resource.disk` are falsy. const resource = baseResource({}); - expect(getResourceMetricsHistoryFallbackMetrics(resource)).toStrictEqual({ + expect(getResourceMetricsHistoryCurrentMetrics(resource)).toStrictEqual({ cpu: undefined, memory: undefined, disk: undefined, @@ -158,37 +158,37 @@ describe('getResourceMetricsHistoryFallbackMetrics branch coverage', () => { it('uses memory.current when total/used are absent', () => { // getMemoryPercent: `memory.total && memory.used` is falsy -> returns current. const resource = baseResource({ memory: { current: 45.5 } }); - expect(getResourceMetricsHistoryFallbackMetrics(resource).memory).toBe(45.5); + expect(getResourceMetricsHistoryCurrentMetrics(resource).memory).toBe(45.5); }); it('uses disk.current when total/used are absent', () => { // getDiskPercent: `disk.total && disk.used` is falsy -> returns current. const resource = baseResource({ disk: { current: 67 } }); - expect(getResourceMetricsHistoryFallbackMetrics(resource).disk).toBe(67); + expect(getResourceMetricsHistoryCurrentMetrics(resource).disk).toBe(67); }); it('coerces a NaN memory.current to undefined via finiteMetric', () => { // getMemoryPercent returns NaN; finiteMetric: typeof === 'number' but // Number.isFinite is FALSE -> undefined. const resource = baseResource({ memory: { current: Number.NaN } }); - expect(getResourceMetricsHistoryFallbackMetrics(resource).memory).toBeUndefined(); + expect(getResourceMetricsHistoryCurrentMetrics(resource).memory).toBeUndefined(); }); it('coerces a NaN disk.current to undefined via finiteMetric', () => { const resource = baseResource({ disk: { current: Number.NaN } }); - expect(getResourceMetricsHistoryFallbackMetrics(resource).disk).toBeUndefined(); + expect(getResourceMetricsHistoryCurrentMetrics(resource).disk).toBeUndefined(); }); it('coerces an Infinity cpu.current to undefined via finiteMetric', () => { const resource = baseResource({ cpu: { current: Number.POSITIVE_INFINITY }, }); - expect(getResourceMetricsHistoryFallbackMetrics(resource).cpu).toBeUndefined(); + expect(getResourceMetricsHistoryCurrentMetrics(resource).cpu).toBeUndefined(); }); it('coerces a NaN cpu.current to undefined via finiteMetric', () => { const resource = baseResource({ cpu: { current: Number.NaN } }); - expect(getResourceMetricsHistoryFallbackMetrics(resource).cpu).toBeUndefined(); + expect(getResourceMetricsHistoryCurrentMetrics(resource).cpu).toBeUndefined(); }); it('coerces a non-number cpu.current (malformed payload) to undefined', () => { @@ -197,7 +197,7 @@ describe('getResourceMetricsHistoryFallbackMetrics branch coverage', () => { const resource = baseResource({ cpu: { current: 'busy' as unknown as number }, }); - expect(getResourceMetricsHistoryFallbackMetrics(resource).cpu).toBeUndefined(); + expect(getResourceMetricsHistoryCurrentMetrics(resource).cpu).toBeUndefined(); }); it('coerces non-finite network and diskIO rates to undefined', () => { @@ -208,7 +208,7 @@ describe('getResourceMetricsHistoryFallbackMetrics branch coverage', () => { writeRate: Number.NaN, }, }); - expect(getResourceMetricsHistoryFallbackMetrics(resource)).toStrictEqual({ + expect(getResourceMetricsHistoryCurrentMetrics(resource)).toStrictEqual({ cpu: undefined, memory: undefined, disk: undefined, @@ -248,7 +248,7 @@ describe('getResourceMetricsHistoryFallbackMetrics branch coverage', () => { }, }); - const metrics = getResourceMetricsHistoryFallbackMetrics(resource); + const metrics = getResourceMetricsHistoryCurrentMetrics(resource); expect(metrics.gpu).toBe(80); expect(metrics.gpu_memory).toBe(87.5); expect(metrics.gpu_temperature).toBe(70); @@ -269,7 +269,7 @@ describe('getResourceMetricsHistoryFallbackMetrics branch coverage', () => { 'disk-io', 'thermals', ]); - expect(getResourceMetricsHistoryFallbackMetrics(resource).temperature).toBe(57.5); + expect(getResourceMetricsHistoryCurrentMetrics(resource).temperature).toBe(57.5); }); it('keeps workload history groups free of host-only thermals', () => { @@ -290,7 +290,7 @@ describe('getResourceMetricsHistoryFallbackMetrics branch coverage', () => { // Mixes a present disk (TRUE arm) with an absent memory (FALSE arm) to prove // the two ternaries are evaluated independently. const resource = baseResource({ disk: { current: 18 } }); - const result = getResourceMetricsHistoryFallbackMetrics(resource); + const result = getResourceMetricsHistoryCurrentMetrics(resource); expect(result.memory).toBeUndefined(); expect(result.disk).toBe(18); }); diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts index bfd109a65..eb92353c8 100644 --- a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts +++ b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerMetricsHistoryModel.ts @@ -107,7 +107,7 @@ export const getResourceMetricsHistoryTarget = ( export const resourceSupportsMetricsHistory = (resource: Resource): boolean => getResourceMetricsHistoryTarget(resource) !== null; -export const getResourceMetricsHistoryFallbackMetrics = ( +export const getResourceMetricsHistoryCurrentMetrics = ( resource: Resource, ): Record => { return { diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts index f2d6be79b..ecf6f8a31 100644 --- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts +++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts @@ -61,7 +61,7 @@ import { hasRuntimeOperationalContext as buildHasRuntimeOperationalContext, } from './resourceDetailDrawerOperationalModel'; import { - getResourceMetricsHistoryFallbackMetrics, + getResourceMetricsHistoryCurrentMetrics, getResourceMetricsHistoryGroups, getResourceMetricsHistoryTarget, resourceSupportsMetricsHistory, @@ -344,8 +344,8 @@ export const useResourceDetailDrawerDerivedState = ( const hasKubernetesDetails = createMemo(() => kubernetesDetailSections().length > 0); const kubernetesDetailsSummary = createMemo(() => buildKubernetesDetailsSummary(resource)); const metricsHistoryTarget = createMemo(() => getResourceMetricsHistoryTarget(resource)); - const metricsHistoryFallbackMetrics = createMemo(() => - getResourceMetricsHistoryFallbackMetrics(resource), + const metricsHistoryCurrentMetrics = createMemo(() => + getResourceMetricsHistoryCurrentMetrics(resource), ); const metricsHistoryGroups = createMemo(() => getResourceMetricsHistoryGroups(resource)); const hasMetricsHistory = createMemo(() => resourceSupportsMetricsHistory(resource)); @@ -478,7 +478,7 @@ export const useResourceDetailDrawerDerivedState = ( hasServiceDetails, serviceDetailsSummary, metricsHistoryTarget, - metricsHistoryFallbackMetrics, + metricsHistoryCurrentMetrics, metricsHistoryGroups, hasMetricsHistory, hasDiscoveryTab, diff --git a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx index e4dd22958..58f43f6f8 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx @@ -496,6 +496,27 @@ describe('GuestDrawer', () => { expect(screen.queryByText('Open related infrastructure')).toBeNull(); }); + it('shows current readings without fabricating history points', async () => { + chartsApiMocks.getMetricsHistory.mockResolvedValue({ + resourceType: 'vm', + resourceId: 'inst1:node1:100', + range: '24h', + start: 0, + end: 0, + metrics: {}, + source: 'store', + }); + render(() => ); + + await fireEvent.click(screen.getByText('History')); + await waitFor(() => expect(chartsApiMocks.getMetricsHistory).toHaveBeenCalled()); + + const utilizationChart = screen.getAllByTestId('guest-history-group-chart')[0]; + expect(utilizationChart).toHaveTextContent('CPU25.0%'); + expect(utilizationChart).toHaveTextContent('Collecting history'); + expect(utilizationChart.querySelector('path')).toBeNull(); + }); + it('updates grouped metric header values on History chart hover', async () => { render(() => ); @@ -627,6 +648,19 @@ describe('GuestDrawer', () => { expect(screen.getByText('QEMU 5.2.0')).toBeInTheDocument(); }); + it('identifies an API-mapped in-guest Pulse Agent without calling it QEMU', () => { + render(() => ( + + )); + + expect(screen.getByText('Pulse Agent')).toBeInTheDocument(); + expect(screen.getByText('Pulse 6.2.0')).toHaveAttribute('title', 'Pulse Agent 6.2.0'); + expect(screen.queryByText(/QEMU/)).toBeNull(); + }); + it('shows plain agent version for containers', () => { render(() => ( diff --git a/frontend-modern/src/components/Workloads/GuestDrawer.tsx b/frontend-modern/src/components/Workloads/GuestDrawer.tsx index 926719258..d14787944 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawer.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawer.tsx @@ -13,7 +13,7 @@ import { DrawerSubjectHeading } from '@/components/shared/DrawerSubjectHeading'; import { DiscoveryReadinessBadge } from '@/components/shared/DiscoveryReadinessBadge'; import { Subtabs, type SubtabOption } from '@/components/shared/Subtabs'; import { getSimpleStatusIndicator } from '@/utils/status'; -import { getGuestDrawerHistoryFallbackMetrics, type GuestDrawerProps } from './guestDrawerModel'; +import { getGuestDrawerCurrentMetrics, type GuestDrawerProps } from './guestDrawerModel'; import { useGuestDrawerState } from './useGuestDrawerState'; import { GuestDrawerHistory, GuestDrawerHistoryRangeSelect } from './GuestDrawerHistory'; import { GuestDrawerOverview } from './GuestDrawerOverview'; @@ -21,6 +21,7 @@ import { GuestDrawerOverview } from './GuestDrawerOverview'; export const GuestDrawer: Component = (props) => { const { activeTab, + agentHeading, agentLabel, agentTitle, agentContextCopied, @@ -58,9 +59,7 @@ export const GuestDrawer: Component = (props) => { workloadActionAgentTitle, } = useGuestDrawerState(props); const headingId = () => `guest-drawer-heading-${guestId()}`; - const historyFallbackMetrics = createMemo(() => - getGuestDrawerHistoryFallbackMetrics(props.guest), - ); + const historyCurrentMetrics = createMemo(() => getGuestDrawerCurrentMetrics(props.guest)); const headerIndicator = createMemo(() => getSimpleStatusIndicator(props.guest.status)); @@ -136,6 +135,7 @@ export const GuestDrawer: Component = (props) => { guest={props.guest} guestId={guestId()} guestOsSummary={guestOsSummary()} + agentHeading={agentHeading()} agentLabel={agentLabel()} agentTitle={agentTitle()} hasAgentInfo={hasAgentInfo()} @@ -164,7 +164,7 @@ export const GuestDrawer: Component = (props) => {
)} diff --git a/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx b/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx index 1b62a8f48..8be16f423 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx @@ -12,6 +12,7 @@ import { FormSelect } from '@/components/shared/FormSelect'; import { filterSelectClass } from '@/components/shared/FilterToolbar'; import { HISTORY_CHART_RANGES, + formatHistoryChartTooltipValue, formatHistoryChartTimeLabel, } from '@/components/shared/historyChartModel'; import { createNonSuspendingQuery } from '@/hooks/createNonSuspendingQuery'; @@ -30,7 +31,7 @@ import { } from './guestDrawerModel'; interface GuestDrawerHistoryProps { - fallbackMetrics?: Record; + currentMetrics?: Record; groups?: GuestDrawerHistoryGroupConfig[]; range: HistoryTimeRange; target: GuestDrawerHistoryTarget | null; @@ -48,6 +49,7 @@ interface GuestDrawerHistoryQueryKey { } interface GuestDrawerHistoryGroupChartProps { + currentMetrics?: Record; group: GuestDrawerHistoryGroupConfig; loading: boolean; metrics: Record; @@ -189,32 +191,6 @@ const findClosestGuestDrawerHistoryPoint = ( return closest; }; -const buildFallbackHistoryPoints = (value: number): AggregatedMetricPoint[] => { - if (!Number.isFinite(value)) return []; - const end = Date.now(); - return [ - { timestamp: end - 60_000, value, min: value, max: value }, - { timestamp: end, value, min: value, max: value }, - ]; -}; - -const mergeFallbackHistoryMetrics = ( - metrics: Record, - fallbackMetrics: Record | undefined, -): Record => { - if (!fallbackMetrics) return metrics; - - let next: Record | null = null; - for (const [metric, value] of Object.entries(fallbackMetrics)) { - if (typeof value !== 'number' || !Number.isFinite(value)) continue; - const existing = metrics[metric] ?? []; - if (existing.length >= 2) continue; - next ??= { ...metrics }; - next[metric] = buildFallbackHistoryPoints(value); - } - return next ?? metrics; -}; - const GuestDrawerHistoryGroupChart: Component = (props) => { const [hoverTimestamp, setHoverTimestamp] = createSignal(null); const series = createMemo(() => @@ -256,9 +232,16 @@ const GuestDrawerHistoryGroupChart: Component const displaySeries = createMemo(() => series().map((item) => { const hovered = hoveredByMetric().get(item.metric); + const currentValue = props.currentMetrics?.[item.metric]; return { ...item, - displayPoints: hovered ? [hovered.point] : item.points, + valueLabel: hovered + ? getGuestDrawerHistoryValueLabel([hovered.point], item.unit) + : item.points.length > 0 + ? getGuestDrawerHistoryValueLabel(item.points, item.unit) + : typeof currentValue === 'number' && Number.isFinite(currentValue) + ? formatHistoryChartTooltipValue(currentValue, item.unit) + : '-', }; }), ); @@ -314,7 +297,7 @@ const GuestDrawerHistoryGroupChart: Component {item.label} - {getGuestDrawerHistoryValueLabel(item.displayPoints, item.unit)} + {item.valueLabel} )} @@ -459,9 +442,6 @@ export const GuestDrawerHistory: Component = (props) => }); const metrics = createMemo(() => historyQuery.value().metrics ?? {}); - const displayMetrics = createMemo(() => - mergeFallbackHistoryMetrics(metrics(), props.fallbackMetrics), - ); const groups = createMemo(() => props.groups ?? GUEST_DRAWER_HISTORY_GROUPS); const errorText = createMemo(() => (historyQuery.error() ? 'Failed to load history data' : '')); @@ -494,7 +474,8 @@ export const GuestDrawerHistory: Component = (props) => )} diff --git a/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx b/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx index b1f35363f..6f061ecb7 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx @@ -29,6 +29,7 @@ interface GuestDrawerOverviewProps { guest: GuestDrawerProps['guest']; guestId: string; guestOsSummary: string; + agentHeading: string; agentLabel: string; agentTitle: string; hasAgentInfo: boolean; @@ -137,7 +138,13 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) { {props.guest.cpus}
- 0}> + 0 + } + >
Uptime @@ -153,7 +160,7 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) {
- Guest agent + {props.agentHeading} {props.agentLabel} diff --git a/frontend-modern/src/components/Workloads/GuestRow.tsx b/frontend-modern/src/components/Workloads/GuestRow.tsx index a6760ef14..90679e570 100644 --- a/frontend-modern/src/components/Workloads/GuestRow.tsx +++ b/frontend-modern/src/components/Workloads/GuestRow.tsx @@ -103,6 +103,9 @@ export function GuestRow(props: GuestRowProps) { const cpuPercent = createMemo(() => getWorkloadCPUPercent(props.guest.cpu) ?? 0); const metricDisplayMode = createMemo(() => props.metricDisplayMode ?? 'bars'); const isSparklineMode = createMemo(() => metricDisplayMode() === 'sparklines'); + const telemetryAvailable = ( + metric: keyof NonNullable, + ): boolean => props.guest.telemetryAvailability?.[metric] ?? true; const isHostMemoryBasis = createMemo(() => props.memoryDisplayBasis === 'host'); const hostMemoryTotal = createMemo(() => { const total = props.parentMemoryTotal; @@ -113,6 +116,7 @@ export function GuestRow(props: GuestRowProps) { ); const memoryDisplayUnavailable = createMemo( () => + !telemetryAvailable('memory') || props.guest.memory?.usageUnavailable === true || (isHostMemoryBasis() && hostMemoryTotal() <= 0), ); @@ -347,27 +351,32 @@ export function GuestRow(props: GuestRowProps) { - -
- } + when={telemetryAvailable('cpu')} + fallback={
} > - {renderMetricSparkline( - 'cpu', - formatMetricPercent(cpuPercent()), - `${props.guest.name} CPU history`, - '%', - 'inline', - formatMetricPercent, - )} + + +
+ } + > + {renderMetricSparkline( + 'cpu', + formatMetricPercent(cpuPercent()), + `${props.guest.name} CPU history`, + '%', + 'inline', + formatMetricPercent, + )} +
@@ -498,7 +507,7 @@ export function GuestRow(props: GuestRowProps) {