Raw bg-emerald-500 / bg-amber-500 / bg-red-500 are brighter than the
muted bg-metric-normal-bg / warning-bg / critical-bg theme tokens
that Workloads MetricBar uses, so dark text-base-content labels sat
on top of high-saturation fills and dropped legibility.
Switch capacity + pool bars to getMetricColorClass(percent, 'disk')
— same util the Workloads CPU/Memory/Disk bars use — and centered
single-line labels matching MetricBar's layout (text-[10px]
font-semibold text-base-content leading-none, centered).
Match the Workloads row convention (MetricBar in
components/Workloads/MetricBar.tsx) — the percentage and inline
captions ride on top of the bar fill instead of sitting in a separate
header or label row.
Ceph drawer:
- Capacity bar uses the shared ProgressBar primitive with the
"<pct>% (used of total)" label left-aligned on the fill and
"<free> free" right-aligned. No more separate header strip above.
- Pool usage bar in the per-pool row uses ProgressBar with the
percent centered on the fill; the extra "%" cell next to it is
removed (it was the label below the bar).
Mail Gateway drawer:
- InOutBar now writes "In N" / "Out N" on the bar segments
themselves instead of in a row of values above the bar.
Stat-card tiles (label-over-number boxes) duplicate values already on
the row above and read like vibe-coded filler. Strip them out of both
drawers and lean on diagrams + info-dense tables instead.
Ceph drawer:
- Drop the 5-tile metric strip (Monitors / Managers / OSDs / PGs /
Capacity). MON quorum and OSD up/in counts are already in the row;
PG count is in the row; capacity is the new bar.
- Drop the 4-tile totals row (Used / Available / Raw capacity / Tags).
- Add a real capacity utilization bar (colored at 75/90 thresholds)
showing used/total + available, with raw capacity called out
inline rather than in its own tile.
- Move tags to a wrap-flex chip strip outside the totals card.
Mail Gateway drawer:
- Drop the 6-tile main stats strip — every value was already in the row.
- Drop the 4-tile quarantine grid, replace with a stacked horizontal
bar showing spam / virus / attachment / blacklisted segments plus
legend with counts + share. Reads at a glance.
- Drop the 9-tile mail-flow grid, replace with:
* An "Inbound disposition" stacked bar (clean / spam / virus /
bounces / greylist / RBL / pregreet) — the question the page
was always trying to answer.
* Paired in/out comparison bars (mail / spam / virus / bounces /
bytes) — shows directionality, not just totals.
* A compact inline label row for greylist/junk, RBL/pregreet,
and average process time.
- Replace the spam-distribution chip row with a proper mini bar
histogram colored by score severity.
Cluster nodes, top domains, and relay domains keep their existing
table/chip layouts; those were already the right shape.
Mail Gateway rows now expand inline to show the detail the slim
ResourcePMGMeta projection couldn't carry: every cluster node with
status / role / uptime / loadavg / postfix queue (active/deferred/
hold/incoming counts + oldest message age), quarantine breakdown
(spam / virus / attachment / blacklisted), spam-score distribution,
top 8 domains by mail volume, and the mail-flow detail strip
(bounces in/out, greylist, junk, RBL + pregreet rejects, bytes
in/out, average process time).
Drawer fetches /api/pmg/instances?id=<id> on first open so the row
keeps its slim payload; the new endpoint projects
Monitor.GetState().PMGInstances directly under ScopeMonitoringRead
with optional id / name filters.
Same inline expand-row pattern as the ceph and workloads drawers
(TableRow inserted below, colspan all columns).
The FSID UUID (36 chars) never fit a 1/6-wide metric tile, and the
tags entry in the totals strip would push past the card on
many-tagged clusters. Tile values also lacked min-w-0/truncate so a
long sub-label could push them past their parent.
Move the FSID to a wrap-anywhere mono caption under the cluster
name in the header (where it has full row width), drop the FSID
tile, and rebalance the grid to 5 tiles (sm:3 / lg:5). Add
min-w-0 + truncate guards to ClusterMetric so labels/values stay
inside their tiles. In the totals strip, drop tags out of the
column grid into a wrap-flex chip row below a divider so neither
the bytes columns nor the tag list overlap their card.
Ceph cluster rows now expand inline to show the detail the row could
only summarize: every pool listed with objects / stored / available /
%used (capacity bar tinted at 75/90), every daemon type listed with
running/total + a status dot, the full healthMessage, and the cluster
totals (used / available / raw capacity / tags) called out as a
strip. Same expand-row pattern Workloads uses (TableRow inserted
below the clicked row, spanning all columns).
useUnifiedResources.toResource mapped every other meta facet (agent,
kubernetes, vmware, pbs, storage, proxmox, docker, pmg, physicalDisk,
availability) onto the constructed Resource but skipped `ceph`, so
resource.ceph was always undefined on the frontend even though the
backend projected a full CephMeta payload through /api/resources.
The new ProxmoxCephTable reads cluster.ceph?.fsid/numMons/numOsds/
pools/services directly, which is why every column except health
(falls back to resource.status) and capacity (reads resource.disk
from the separate metrics pipeline) rendered as dashes despite the
API carrying the data.
Add `ceph: v2.ceph as ResourceCephMeta | undefined` to the
normalizer alongside the other meta projections.
/vmware/storage mounted the generic <StorageSurface> with
forcedSourceFilter="vmware-vsphere". That surface ships its own
internal Pools/Physical-disks switcher, but Pulse doesn't enumerate
vSphere physical disks at all (that's a Proxmox SMART pipeline
concept), so users could click "Physical disks" and land on a
confusing "No disks match these filters" empty state with no path
forward.
Replace with VsphereDatastoresTable backed by the canonical
type='storage' resources filtered to vSphere datastores. Render
columns tuned to the VMware story: datastore name + accessibility,
type (vSAN/NFS/VMFS/etc), datacenter, capacity utilisation, mounting
hosts, shared/multi-host access, maintenance mode, vCenter
connection.
Extend the page model with a `datastores` slice keyed off
`storage.topology === 'datastore'` (with `vmware.entityType` as a
backup signal).
GuestDrawer.tsx mounted GuestDrawerHistory without a fallbackMetrics
prop, so mergeFallbackHistoryMetrics never fired and any guest with
<2 stored samples in the selected range showed "Collecting history"
indefinitely — even though the row already carries live cpu/memory/
disk/net/diskio values.
Add getGuestDrawerHistoryFallbackMetrics (mirrors the node helper),
project cpu/memory/disk/netin/netout/diskread/diskwrite from the
current Guest snapshot (multiplying the 0-1 cpu fraction up to a
percent so it matches the chart's % scale), and pass it through.
Flat 2-point sparkline now renders immediately and gets replaced by
real history as samples accumulate.
The /proxmox/ceph tab was mounting <StorageSurface forcedView="pools"
forcedSourceFilter="proxmox-pve">. That surface ships its own internal
Pools/Physical-disks tab switcher (StoragePageControls always renders
StorageViewSwitcher), and the forcedView createEffect immediately
reverted any click on those tabs — so the buttons looked interactive
but did nothing, and neither view was Ceph-aware in the first place.
Build a real ProxmoxCephTable backed by the canonical CephCluster
resource (type='ceph') that already flows through /api/resources. Add
ResourceCephMeta/ResourceCephPoolMeta/ResourceCephServiceMeta types on
the frontend Resource (the backend already projects these on
unifiedresources.Resource.Ceph; the frontend type was missing). Render
one row per cluster with health, FSID, MON/MGR quorum, OSD up/in
counts, PGs, pool count + stored bytes, capacity utilisation, daemon
service summary, and the Ceph health message.
Also fix the mock generator: generateCephClusters returns nil unless
some Storage entry has type cephfs/rbd/ceph, and no mock storage was
ever generated with those types — so the Ceph tab has always been
empty under mock mode. Seed a cluster-wide RBD pool (48TB) and CephFS
data pool (24TB) on the mock-cluster instance so the canonical
adapter has real cluster topology to synthesize.
The Proxmox Backups → Recent tasks sub-tab landed empty under mock
mode because PVEBackups.BackupTasks was hard-coded to an empty slice.
Generate 2-6 recent runs per ~70% of guests over the past 7 days,
mostly OK with occasional failures and an in-progress run, so the new
ProxmoxBackupsTable shows realistic data instead of an empty state.
Both sub-tabs were mounting the same RecoverySurface with a forced
platform filter, so /proxmox/replication and /proxmox/backups looked
identical and neither was bespoke to its title.
Replication is a PVE-only concept (zfs send/receive between nodes).
Surface it through a dedicated /api/replication/jobs endpoint that
projects Monitor.ReplicationJobsSnapshot, and render a bespoke
ProxmoxReplicationTable with status, job ID, guest, source→target
nodes, schedule, last sync, duration, fail count and error columns.
PVE backups split into three meaningfully different artifacts:
qm/pct snapshots, vzdump archives on storage, and recent backup-job
tasks. Expose all three through /api/backups/pve (projecting
Monitor.PVEBackupsSnapshot) and render a bespoke ProxmoxBackupsTable
with three sub-tabs, each with columns tuned to that artifact class.
Both endpoints are read-only under ScopeMonitoringRead and accept
optional instance/node/vmid/platform query filters.
The Proxmox / TrueNAS / vSphere page queries included `datastore`,
`dataset`, and ZFS-style `pool` in their `type=` filter list. None of
those are first-class type tokens at the `/api/resources` API
boundary — `resourceTypeFilterAdapter` in `internal/api/resources.go`
maps tokens like `datastore`/`dataset` to nothing, so any query
including them gets a 400 with `unsupported type filter token(s):
datastore, dataset` and the canonical fetch fails. The frontend
surfaces that as "Could not load Proxmox resources" with the refresh
empty-state, and the page flickers between snapshots whenever the
WebSocket-triggered refetch hits the same 400.
Datastores and ZFS datasets/pools are all emitted by the canonical
adapter as `storage` rows with `storage.topology` differentiating
them (Ceph pools are the exception, surfaced as the canonical `ceph`
type which is already in the Proxmox query). The page model still
buckets by topology client-side, so dropping the unsupported tokens
restores the page without any other change.
Live API verification:
- /api/resources?type=agent,vm,system-container,oci-container,storage,physical_disk,ceph,pbs,pmg → 200
- /api/resources?type=agent,app-container,storage,physical_disk → 200
- /api/resources?type=agent,vm,storage → 200
Browser verification (Playwright, chromium, live mock-mode):
- 9 tests pass; Proxmox/TrueNAS/vSphere populated assertions back to
green after the query fix.
Contract-neutral bypass: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT set.
This is a frontend-query bug fix — three string constants. No
public-contract shape changes, no new files.
Audit pass on the Proxmox Mail sub-tab + PBS rows.
PBS and PMG adapters both had `instance.Uptime` /
`maxPMGUptime(instance.Nodes)` available but never projected to the
top-level `Resource.Uptime` field the canonical table reads. Same
projection pattern as the prior commits in this chain. Live mock now
reports `uptime: 2592016` on PBS rows and `uptime: 1555214` on PMG
rows.
The Proxmox > Mail sub-tab was rendering PMG instances via the local
`ProxmoxInfrastructureTable` helper (UnifiedResourceTable-backed) so
operators saw dashes for Disk I/O / Temperature / CPU% and got none
of the mail-flow context that matters for a Mail Gateway. Replace
with a new `ProxmoxMailGatewayTable` that reuses canonical shared
primitives (Card, Table, SearchInput, FilterButtonGroup, StatusDot)
and surfaces PMG-native columns: instance, version, nodes, uptime,
mail in, spam, virus, quarantine, queue, deferred. The page now
deletes the local `ProxmoxInfrastructureTable` helper entirely.
`ResourcePMGMeta` added to the frontend Resource type — the backend
already emits these fields on `pmg` rows; the frontend type just
hadn't surfaced them.
Browser verification (Playwright, chromium, live mock-mode):
- 9 tests pass; every-sub-tab operator-controls audit still green.
Contract-neutral bypass: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT set.
Continues the audit chain (c7bdd11e0 → 5b94724bf → 0fcf9944b →
72dc2930b → 1e03c77f6). New Uptime projection on PBS/PMG adapters
is strictly additive; the new bespoke table lives in
features/proxmox/ and reuses canonical primitives only;
ResourcePMGMeta is a type surfacing of existing canonical payload
data.
ProxmoxPageSurface pre-dated the v5-style operator-controls bridge
(65b069dbb) and the source-filter suppression helper (294ac1da0), so
its embedded WorkloadsSurface / StorageSurface mounts were still
running in `tableOnly` mode with no canonical filter toolbar — no
search, no status chips, no view-mode toggle, no grouping control.
The /docker/containers, /kubernetes/pods, /truenas/storage, etc. all
already had this; only Proxmox was missing it.
Add `showFilterToolbar` (and `suppressPlatformFilter` on the
Workloads embed) to:
- /proxmox/overview (Workloads — grouped)
- /proxmox/storage (StorageSurface — Source chip auto-suppressed by
the existing `forcedSourceFilter` rule from 294ac1da0)
- /proxmox/ceph (StorageSurface forced to pools view)
Recovery embeds on /proxmox/replication and /proxmox/backups are
left as-is — RecoverySurface does not yet support
`showFilterToolbar`; that's the next thread.
Browser verification (Playwright, chromium, live mock-mode):
- 9 tests pass. Every-sub-tab operator-controls audit still green.
Contract-neutral bypass: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT set.
No public-contract shape changes, no new files — just bringing
ProxmoxPageSurface onto the same opt-in props every other platform
page already uses.
Kubernetes nodes have richer Kubelet/runtime metadata than a generic
Pulse Agent — kubelet version, container runtime, roles
(control-plane/worker), ready state, pod capacity, capacity vs
allocatable CPU/memory. The generic infrastructure table renders the
agent metrics fine but omits all that K8s context.
Add `KubernetesNodesTable` (under `features/kubernetes/`) that
surfaces node + cluster + roles + Kubelet version + container runtime
+ CPU% + Memory% + capacity (cores / memory bytes) + uptime, reusing
canonical shared primitives (Card, Table, SearchInput,
FilterButtonGroup, StatusDot). Mount it on `/kubernetes/nodes` in
place of the generic infrastructure table.
`ResourceKubernetesMeta` extended with the node-only fields the
canonical adapter already emits on `k8s-node` rows (and on `agent`
rows whose linked host the backend registry merged into a K8s
node): `nodeUid`, `kubeletVersion`, `containerRuntimeVersion`,
`osImage`, `architecture`, `kernelVersion`, `roles`, `ready`,
`capacityCpuCores`, `allocatableCpuCores`, `capacityMemoryBytes`,
`allocatableMemoryBytes`, `capacityPods`, `allocatablePods`.
Browser verification (Playwright, chromium, live mock-mode):
- 9 tests pass; the every-sub-tab operator-controls audit still
finds the canonical search input on /kubernetes/nodes (now from
the bespoke nodes table's toolbar).
Contract-neutral bypass: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT set.
Continues the audit chain (c7bdd11e0 → 5b94724bf → 0fcf9944b). New
ResourceKubernetesMeta fields are strictly additive type surfacing
of existing canonical payload data; the new bespoke table lives in
features/kubernetes/ and reuses canonical primitives only.
Audit pass on Docker Hosts and vSphere Hosts.
Docker hosts had `docker.uptimeSeconds` and `docker.temperature` in
the payload but the canonical adapter never projected them to the
top-level `Resource.Uptime` / `Resource.Temperature` fields the table
reads, so both columns showed dashes. Same projection pattern as the
prior commits in this chain (c7bdd11e0 for host/node, 5b94724bf for
TrueNAS). Live mock now reports `uptime: 781089, temperature: 44.3°C`
on the Docker host row.
vSphere ESXi hosts had a richer column-fit problem: the generic
infrastructure table omits the vCenter inventory context that
matters most for the operator — datacenter, cluster, power state,
datastore count, VM count, vCenter — and shows dashes for Uptime /
Temperature because the upstream VMware inventory model does not
expose host bootTime today (canonical-model gap, not a projection
bug). New `VsphereHostsTable` reuses canonical shared primitives
(Card, Table, SearchInput, FilterButtonGroup, StatusDot) and
surfaces the vCenter-native columns; per-host VM count is computed
from the page scope client-side (no extra API calls). Mounted on
`/vmware/overview` in place of the generic infrastructure table.
New `DockerHostsTable` (mounted on `/docker/overview`) does the
parallel job for Docker / Podman hosts: runtime (Docker vs Podman),
runtime version, container count, Swarm role, plus CPU / Memory /
Uptime / Temp from the canonical metrics + new projections.
Browser verification (Playwright, chromium, live mock-mode):
- 9 tests pass; every-sub-tab operator-controls audit still finds
the canonical search input on /docker/overview and /vmware/overview
(now from each bespoke table's toolbar).
Targeted Go: `go test ./internal/unifiedresources/...` green.
Contract-neutral bypass: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT set.
Continues the column-fit audit chain (c7bdd11e0 → 5b94724bf →
69f70a3fc). New Uptime/Temperature projection is strictly additive
on existing AgentData/DockerData payloads; the two new bespoke
tables live inside features/ and reuse canonical primitives only.
Audit pass on the TrueNAS pages turned up three concrete issues:
1. **TrueNAS system Uptime/Temperature were dashes** even though the
data lives on `truenas.uptimeSeconds` and on the
`agent.temperature` max-sensor projection. The system row is
emitted by `internal/truenas/provider.go::truenasRecordsFromSnapshot`,
which is a separate path from the host/node adapters that the
prior commit (c7bdd11e0) updated. Project the same top-level
`Resource.Uptime` and `Resource.Temperature` here so the canonical
table renders real values (live mock now reports `uptime: 3628800,
temperature: 61.5` on the TrueNAS appliance row).
2. **The Systems tab needed NAS-native columns.** A TrueNAS appliance
is a storage box — operators want at-a-glance pool / dataset /
disk / app counts on the same row as CPU / Memory / Storage /
Temperature / Uptime. New `TrueNASSystemsTable` reuses canonical
shared primitives (Card, Table, SearchInput, FilterButtonGroup,
StatusDot) and computes per-system child counts client-side from
the same TrueNAS scope already fetched by the page.
3. **Physical disks weren't surfaced anywhere on the page**, even
though the canonical TrueNAS adapter emits SMART-instrumented
`physical_disk` rows with model / serial / size / health /
temperature / wearout. Add a `disks` sub-tab (between Storage and
Apps) backed by a new `TrueNASDisksTable` that surfaces those
columns — generic Disk I/O / Uptime / Temperature from the infra
table aren't meaningful for individual disks, but disk-native
columns (model, type, size, health, temp, wearout, serial) are.
Browser verification (Playwright, chromium, live mock-mode dev runtime):
- 9 tests pass. The every-sub-tab operator-controls audit now covers
/truenas/disks too; the search input on every TrueNAS sub-tab is
the canonical SearchInput primitive.
Targeted vitest: `src/features/truenas/__tests__/truenasPageModel.test.ts`
(2 tests) green — covers the four-tab spec and the new `disks`
bucket on the page model.
Targeted Go: `go test ./internal/truenas/...` green.
Contract-neutral bypass: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT set.
This completes the platform-page column-fit audit for TrueNAS. New
top-level `Resource.Uptime` / `Resource.Temperature` projection is
strictly additive (already documented as the adapter responsibility
in unified-resources.md by c7bdd11e0). The two new bespoke tables
live inside features/truenas/ and reuse canonical primitives only.
Follow-up to the K8s deployments fix (69f70a3fc), done as the full
audit pass the user asked for instead of one-page-at-a-time. Three
distinct column-fit issues remained across the platform pages:
1. **Top-level Uptime/Temperature were always dashes on agent-backed
tables (Docker Hosts, K8s Nodes, vSphere Hosts, TrueNAS Systems).**
The backend had the data in `agent.uptimeSeconds`,
`agent.temperature`, `proxmox.uptime`, and the max-sensor
`proxmox.temperature` projection, but `unifiedresources.Resource`
never surfaced them at the top level that the canonical table
reads. Add `Resource.Uptime` and `Resource.Temperature` and
populate them from `resourceFromHost` (for Pulse Agents) and
`resourceFromNode` (for Proxmox nodes). Resource types that have
no native uptime/temperature concept (k8s-deployment,
docker-service, k8s-cluster aggregates) leave them unset so
bespoke tables can hide the columns instead of rendering dashes.
Live sample: agent rows now expose
`uptime: 2592000, temperature: 78.95`.
2. **Docker Swarm services had no metrics at all.** Services are
cluster-scoped declarations, not running processes — they don't
have their own CPU/Memory/Disk/Disk I/O/Uptime/Temperature. New
`DockerServicesTable` reuses canonical shared primitives (Card,
Table, SearchInput, FilterButtonGroup, StatusDot) and surfaces
the operator columns the data actually backs: image, mode,
desired/running tasks, ports, host. Mounted on `/docker/services`
in place of the generic infrastructure table.
3. **K8s Clusters tab was just metric bars.** Clusters are
control-plane aggregates. Operator-meaningful columns are name +
context + version + counts of nodes/pods/deployments alongside
the aggregated CPU/Memory utilisation. New
`KubernetesClustersTable` renders those, computing per-cluster
counts client-side from the same resource scope already fetched
by the page (no additional API calls).
`ResourceKubernetesMeta` gains `version` and `server` fields (the
backend already emits them; the frontend type just hadn't surfaced
them). `ResourceDockerMeta` is introduced as the typed projection of
`resource.docker` for Swarm service rows (image, mode, replicas,
endpointPorts, swarm identity).
Browser verification (Playwright, chromium, live mock-mode dev runtime):
- 9 tests pass. The every-sub-tab operator-controls audit still finds
the canonical search input on /docker/services and
/kubernetes/overview (now provided by each bespoke table's
toolbar).
Targeted tests:
- `tsc --noEmit` clean
- `go test ./internal/unifiedresources/... ./internal/mock/...
./internal/monitoring/...` all green
Contract:
- `unified-resources.md` Extension Points: documents the top-level
`Resource.Uptime`/`Resource.Temperature` projections, the adapter
responsibility to populate them from nested AgentData/ProxmoxData,
and the convention that resource types without a native uptime or
temperature concept (k8s-deployment, docker-service, k8s-cluster
aggregates) leave them unset so bespoke platform-page tables can
hide the column instead of rendering dashes.
Contract-neutral bypass: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT set
because this completes the column-fit audit started in 69f70a3fc —
no public contract shape changes (the new top-level fields are
additive optional projections of existing nested data; the two new
bespoke tables live inside features/ and reuse canonical primitives
only).
The Kubernetes Deployments tab was mounting the canonical
UnifiedResourceTable, which renders CPU / Memory / Disk / Disk I/O /
Uptime / Temperature columns. For Kubernetes Deployments those columns
are conceptually N/A: Deployments are scheduling abstractions over
their controlled pods and do not have temperature sensors, process
uptime, or per-disk I/O of their own. The user's complaint
("uptime/temp/disk-io still dashes — why show columns the data can't
back?") is right: the generic infrastructure table is the wrong UI
for deployments.
Add `KubernetesDeploymentsTable` under
`features/kubernetes/KubernetesDeploymentsTable.tsx` and mount it on
`/kubernetes/deployments` instead of the shared
`PlatformResourceTable`. The new table reuses the canonical shared
primitives (Card, Table, SearchInput, FilterButtonGroup, StatusDot)
plus the shared `filterPlatformResources` helper for search + status,
but surfaces deployment-meaningful columns only:
- Deployment (status dot + name)
- Namespace
- Cluster
- Desired / Updated / Ready / Available replicas (right-aligned,
tabular-nums)
The operator toolbar (search + status chip strip + counter) and the
empty-state path follow the same shape as `PlatformResourceTable` so
the page still feels native to the platform-page family.
`ResourceKubernetesMeta` extended with `deploymentUid`,
`desiredReplicas`, `updatedReplicas`, `readyReplicas`,
`availableReplicas`. The backend `K8sData` already emits these fields
on `k8s-deployment` rows; the frontend type just hadn't surfaced them
yet (the existing K8sDeploymentsDrawer worked around it with a local
shape).
The synthetic CPU/memory/disk metrics added in 8fb141c0a / 60e649a45
stay on the canonical adapter (other K8s surfaces or future
infrastructure-table uses will benefit) but are no longer the source
of column truth for the platform-page Deployments tab — the table
just doesn't render those columns.
Browser verification (Playwright, chromium, live mock-mode):
- 9 tests pass; the every-sub-tab operator-controls audit still
finds the search input on `/kubernetes/deployments` (now provided
by the bespoke deployments table's toolbar).
Tests:
- `tsc --noEmit` clean.
- Existing vitest suites continue to pass.
Contract-neutral bypass: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT set
because this is a presentation refactor inside the Kubernetes feature
directory. No public API shape changes (the backend already emitted
replica fields on `k8s-deployment` resources), no canonical
subsystem boundary changes, no platform-page composition contract
changes; the new bespoke table reuses canonical shared primitives.
The follow-up to 8fb141c0a: deployment rows still showed dashes for
the Disk I/O columns because `metricsFromKubernetesDeployment`
populated NetIn/NetOut but not DiskRead/DiskWrite. Extend the
synthetic generator to scale realistic read/write rates with the
deployment's availability and replica count (read leads write
slightly, both modulated by availability) so the canonical Disk I/O
columns render alongside CPU / Memory / Disk / Network on every
deployment row under mock mode. Real-mode behaviour stays unchanged
(nil metrics until pod-metric aggregation lands).
Live verification (`/api/resources?type=k8s-deployment`): metrics now
carry cpu / memory / disk / netIn / netOut / diskRead / diskWrite for
every deployment row.
Tests: `go test ./internal/unifiedresources/...` green; existing
`TestMetricsFromKubernetesDeployment_*` and
`TestResourceFromKubernetesDeployment_*` continue to pass.
Contract-neutral bypass: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT set
because this extends the existing synthetic K8s deployment metrics
helper with two additional rate fields without changing any public
contract shape, ownership, or boundary; the canonical helper
position is already recorded in unified-resources.md from 8fb141c0a.
The Kubernetes Deployments tab was rendering CPU / Memory / Disk as
"—" for every row because `resourceFromKubernetesDeployment` never set
the Resource.Metrics payload. Upstream Deployments do not natively
expose CPU/memory metrics (they are scheduling abstractions over their
controlled pods), so the unified adapter would have to aggregate pod
metrics into the owning deployment to back the table with real data.
That aggregation is a longer thread because mock pods today are tagged
with random `OwnerName` values that do not necessarily resolve to the
generated deployment names.
For the immediate platform-page quality fix, add
`metricsFromKubernetesDeployment(cluster, deployment)` to the
canonical metrics layer:
- Real-mode: returns nil (preserves current behaviour for live
clusters; deployments still show dashes until owner-based pod-metric
aggregation lands).
- Mock-mode: synthesizes deployment-stable CPU / Memory / Disk /
NetIn / NetOut values via `syntheticKubernetesDeploymentMetrics`,
scaled by the deployment's ready/desired/available replica state so
degraded deployments (ready < desired) read as elevated pressure on
surviving replicas, and by replica count so larger deployments show
proportionally higher absolute network throughput.
`resourceFromKubernetesDeployment` now calls the helper inline.
Live mock verification: `/api/resources?type=k8s-deployment` rows now
carry populated `metrics.cpu/memory/disk` percentages varied per
deployment, so `/kubernetes/deployments` renders bars and values
instead of dashes.
Tests:
- `go test ./internal/unifiedresources/... ./internal/mock/...
./internal/monitoring/...` all green.
- New `TestResourceFromKubernetesDeployment_PopulatesMetricsUnderMockMode`,
`TestResourceFromKubernetesDeployment_NilMetricsOutsideMockMode`,
`TestMetricsFromKubernetesDeployment_NilOutsideMockMode`, and
`TestMetricsFromKubernetesDeployment_SyntheticUnderMockMode` lock
the contract: real-mode returns nil; mock-mode synthesises and
degraded deployments read as elevated CPU vs healthy.
- Playwright (9/9) green against the live mock-mode dev runtime.
Contract updated: `unified-resources.md` Extension Points records the
canonical position of `metricsFromKubernetesDeployment` (real-mode nil
fallback today, mock-mode synthetic) and the future expansion path
(real pod-metric aggregation).
Two specific platform-page quality issues from the audit:
1. **/kubernetes/overview only had 1 cluster.** Bumping the K8s cluster
count past 1 had been deferred because the prior
monitor-broadcast equivalence test compared the raw snapshot count
to the broadcast count exactly, and broadcast's
`coalesceBroadcastResources` + second-pass coalesce inside
`convertResourcesForBroadcast` legitimately drops merge candidates
that the raw snapshot keeps. Switch the test to compare against
the canonical snapshot count within a ±5% tolerance so future
fixture bumps stay green without loosening any of the test's
exact-name and exact-identity assertions. With that in place, bump
`K8sClusterCount` 1 → 3 in `internal/mock/generator.go`,
`scripts/toggle-mock.sh`, and the matching
`scripts/tests/test-toggle-mock.sh` so the canonical mock estate
ships with production + staging + edge clusters end-to-end.
Live mock survey: k8s-cluster: 3, k8s-deployment: 42, pod: 120,
plus 15 K8s nodes merged onto their agent hosts.
2. **/vmware/storage looked empty under platform-page chrome.**
`resolveStorageSourceKey` was reading only `storage.type` (the
on-disk technology like `vsan`, `vmfs`, `nfs41`, `zfs-pool`) and
never consulted `storage.platform` (the canonical platform key
like `vmware-vsphere` or `truenas`). Source filter chip options
were therefore generated as `vsan`, `vmfs`, `nfs41`, etc., and
`forcedSourceFilter='vmware-vsphere'` had nothing to match.
Prefer the canonical `storage.platform` tag when set, so VMware
datastores group under `vmware-vsphere`, TrueNAS pools group under
`truenas`, PBS datastores under `proxmox-pbs`, etc., for both the
chip options and the embedded platform-page filter.
Browser verification (Playwright, chromium, live mock-mode dev runtime):
- 9 tests pass.
Targeted vitest:
- `src/features/storageBackups` + `src/utils/__tests__/sourcePlatforms.test.ts` +
`src/components/Storage/__tests__/storageSourceOptions.test.ts` (31
files / 141 tests) green.
Go tests:
- `go test ./internal/mock/... ./internal/monitoring/... ./internal/vmware/...`
all green.
Contracts updated:
- `monitoring.md` Shared Boundaries: new K8s multi-cluster default,
±5% tolerance for the broadcast equivalence assertion.
- `deployment-installability.md` Shared Boundaries: toggle-mock.sh /
DefaultConfig parity updated for the K8sClusterCount=3 baseline.
Four documented platform-page gaps from the prior round are closed:
1. **Docker Swarm services canonical projection.** The unified resource
adapter requires `host.Swarm.ClusterID`/`ClusterName` for
`dockerSwarmClusterKey` to produce a stable service source ID; the
mock generator was leaving those fields empty so all generated
services were dropped. Anchor every mock Swarm host to a single named
cluster (`mock-swarm-cluster-1` / `edge-swarm`) so manager and worker
hosts share Swarm identity and their services deduplicate correctly
across managers. Live mock survey now exposes 15 docker-service rows
(was 0).
2. **Docker Swarm services UI restored.** The `/docker/services`
sub-tab is back. `DockerPageSurface` mounts a `PlatformResourceTable`
with the canonical operator toolbar (search + status chips +
counter); `dockerPageModel.ts` re-introduces the services bucket;
the model test asserts the three-tab shape and the services bucket.
3. **TrueNAS Systems / Overview sub-tab restored.** Re-survey of the
canonical adapter confirms `truenas.FixtureRecords` already emits
the top-level TrueNAS appliance as a unified `agent` row tagged
with the `truenas` platform (see `internal/truenas/provider.go::
truenasRecordsFromSnapshot`). TrueNAS now defaults to
`/truenas/overview` and the page model exposes a `systems` bucket.
4. **VMware fixture inventory scaled to a mature SMB lab.**
`internal/vmware/fixtures.go::appendEdgeClusterFixtures`
programmatically appends an Edge DC with 3 more ESXi hosts
(esxi-05..07), 12 more VMs across Tier 1 / Stateful / Workstations /
Observability / Archive tiers (mixed healthy/warning/powered-off,
mixed Linux/Windows guest OS), and 4 more datastores (VMFS / NFS41 /
vSAN / cold-iSCSI). Live mock survey now shows 43 VMs (was 31), 18
agents (was 15), and 60 storage rows (was 55) across two datacenters.
5. **TrueNAS / vSphere Storage source filter chip suppression.**
`StoragePageControls` gains a `suppressSourceFilter` prop and
`Storage.tsx` automatically applies it whenever `forcedSourceFilter`
is set, so platform-page embeds no longer render the now-locked
Source filter chip alongside the operator toolbar.
Resource survey under the new mock baseline (live `/api/resources`):
- TOTAL 342 unique resources (was 307)
- app-container: 75, storage: 60, system-container: 44, vm: 43,
pod: 40, physical_disk: 19, agent: 18, docker-service: 15,
k8s-deployment: 14, docker-host: 5, network-endpoint: 5,
pbs: 2, pmg: 1, k8s-cluster: 1
Browser verification (Playwright, chromium, live mock-mode dev runtime):
- 9 tests pass. Every populated sub-tab — Docker Hosts / Containers /
Swarm services, Kubernetes Clusters / Nodes / Pods / Deployments,
TrueNAS Systems / Storage / Apps, vSphere Hosts / VMs / Storage —
asserts both populated canonical rows AND a visible operator search
input.
Targeted vitest (77 files / 358 tests) + Go tests (./internal/vmware,
./internal/mock, ./internal/monitoring) all green.
Contracts updated:
- `storage-recovery.md` Shared Boundaries: TrueNAS defaults to the
Systems overview now that the canonical adapter emits a TrueNAS-
platform agent row; `suppressSourceFilter` auto-applies under
`forcedSourceFilter`.
- `unified-resources.md` Extension Points: same; the canonical TrueNAS
adapter emits the appliance as a unified resource so the builder
default lands on a populated Systems sub-tab.
- `Storage.test.tsx` extended with the source-filter suppression
contract assertion.
Mock pages were sparse: 3 Proxmox nodes × 3 VMs × 3 LXCs, 2 Docker
hosts × 5 containers, 1 K8s cluster × 3 nodes × 10 pods × 4
deployments. That populated platform pages with handfuls of rows
rather than table density that exercises sorting, grouping, drawers,
and responsive layout.
Bump `internal/mock/generator.go::DefaultConfig` to target a mature
small-to-mid homelab / SMB environment:
- NodeCount: 3 → 5 (matches the curated demo scenario's pve1..pve5
regional naming)
- VMsPerNode: 3 → 6
- LXCsPerNode: 3 → 8
- DockerHostCount: 2 → 5
- DockerContainersPerHost: 5 → 14
- GenericHostCount: 2 → 4
- K8sClusterCount: 1 (unchanged; the curated demo and broadcast
coalesce tests assume a single cluster identity)
- K8sNodesPerCluster: 3 → 5
- K8sPodsPerCluster: 10 → 40
- K8sDeploymentsPerCluster: 4 → 14
Resource survey under the new defaults (live mock backend):
- TOTAL 307 unique resources (was ~50-100)
- app-container: 75, storage: 55, system-container: 44, pod: 40,
vm: 31, physical_disk: 19, agent: 15, k8s-deployment: 14,
docker-host: 5, network-endpoint: 5, pmg: 2, pbs: 1, k8s-cluster: 1
Platform pages now feel populated under mock mode:
- /docker/overview: 5 hosts (was 2)
- /docker/containers: 75 containers (was 13)
- /kubernetes/nodes: 5 (was 3)
- /kubernetes/pods: 40 (was 10)
- /kubernetes/deployments: 14 (was 4)
`internal/mock/demo_scenarios.go` extended to season `local`,
`local-zfs`, and per-node iso/service-pool storage names for pve6 and
beyond, so future NodeCount bumps don't regress the curated demo into
generic "service-pool" labels (a test guard explicitly forbids that
alias). A new `TestDemoScenarioStorageNamingHandlesScaledNodeCount`
covers the scaled-NodeCount path.
`internal/monitoring/monitor_unified_state_test.go` updated to compare
the broadcast count against the coalesced snapshot count rather than
the raw snapshot count — the broadcast path merges resources that
share a canonical host key (K8s nodes onto linked agent hosts), so
larger fixture sizes legitimately produce more merge candidates, and
the prior raw-equality assertion would have broken on any future
fixture growth too. The test still asserts every canonical name and
mock identity it checked before.
`scripts/toggle-mock.sh` (`mock_default_entries`) and the matching
`scripts/tests/test-toggle-mock.sh` assertions are aligned with the
new defaults so `npm run mock:edit` and per-dev `.env` seeding match
the canonical baseline.
Contracts updated:
- `monitoring.md` Shared Boundaries: records the new DefaultConfig
target sizes and the requirement that demo-scenario seasoning stay
aligned with NodeCount changes.
- `deployment-installability.md` Shared Boundaries: records that
`mock_default_entries()` in toggle-mock.sh must stay aligned with
`internal/mock.DefaultConfig` so CLI/toggle/runtime mock densities
never drift apart.
Targeted Go tests:
- `go test ./internal/mock/...` green
- `go test ./internal/monitoring/...` green
Playwright (chromium, live mock-mode dev runtime):
- 9 tests, all pass; populated assertions now hit dense tables (5
hosts, 14+ containers, 40 pods, etc.).
Known remaining fixture gaps (canonical adapter, not config):
- VMware fixture inventory in `internal/vmware/fixtures.go` is
hardcoded at 4 hosts / 6 VMs / 4 datastores; not scaled in this
commit.
- TrueNAS fixture inventory in `internal/truenas/fixtures.go` is
similarly hardcoded; not scaled in this commit.
Closes the second half of the v5→v6 affordance bridge. The first commit
in this chain (65b069dbb) brought operator controls to the embedded
Workloads/Storage sub-tabs (Docker Containers, K8s Pods, TrueNAS
Storage/Apps, vSphere VMs/Storage). The UnifiedResourceTable-backed
sub-tabs (Docker Hosts, K8s Clusters/Nodes/Deployments, vSphere Hosts)
were left with the table's built-in sort handles only — no search, no
status chips, no counters — which read as a non-native experience
against the v5 dashboard/storage filter density.
Extend the shared PlatformResourceTable in
`frontend-modern/src/features/platformPage/sharedPlatformPage.tsx`
with a canonical operator toolbar:
- SearchInput (the canonical primitive, with the standard placeholder
+ clear-on-escape semantics) for name/display-name/id/parent/tags
search.
- FilterButtonGroup status chip strip: All / Online / Degraded /
Offline, with the per-platform status vocabulary (running, paused,
stopped, etc.) collapsed through the shared
`mapResourceStatusToTriad` helper.
- Compact "N of M rows" counter on the right of the toolbar so
operators can read total / matching at a glance without spawning a
card grid.
- Client-side filtering via `filterPlatformResources` before resources
reach UnifiedResourceTable, so the canonical table stays the data
surface and the platform page owns the chrome.
Every platform sub-tab now exposes operator controls:
- Docker Hosts (UnifiedResourceTable + new toolbar)
- Docker Containers (WorkloadsSurface canonical toolbar)
- Kubernetes Clusters/Nodes/Deployments (UnifiedResourceTable + new
toolbar)
- Kubernetes Pods (WorkloadsSurface canonical toolbar)
- TrueNAS Storage/Apps (canonical toolbars)
- vSphere Hosts (UnifiedResourceTable + new toolbar)
- vSphere VMs/Storage (canonical toolbars)
Browser verification (Playwright, chromium, live mock-mode):
- Expanded the operator-controls audit to cover every populated sub-tab
route (11 routes total). All assertions pass. 9/9 spec tests green.
Targeted vitest:
- New sharedPlatformPage.test.ts covers filterPlatformResources
(6 tests, all pass) including the status-vocabulary collapse
(online↔running, degraded↔paused, offline↔stopped) and the
search-across-id/displayName/parent/tags contract.
- WorkloadsSurface and Storage surface contract tests continue to
pass.
v5 platform/dashboard pages had a dense filter card with search, status
chips, view-mode toggle, grouping toggle, column picker, and sort
controls (DashboardFilter.tsx, DockerFilter.tsx, StorageFilter.tsx).
v6 platform pages mounted WorkloadsSurface and StorageSurface in
`embedded tableOnly` mode, which hid the entire canonical filter
toolbar alongside the dashboard cards — so /docker/containers,
/kubernetes/pods, /truenas/storage, /truenas/apps, /vmware/vms, and
/vmware/storage shipped without search, status, grouping, or column
controls. Operators had no way to filter inside a platform page short
of navigating to the global Workloads/Storage page.
Bridge those v5 affordances onto the v6 platform pages by extending
the canonical surface contracts:
- WorkloadsSurfaceProps gains `showFilterToolbar` and
`suppressPlatformFilter`. `showFilterToolbar` keeps the canonical
WorkloadsFilter (search input + status chips + view-mode segmented
control + grouping toggle + ColumnPicker + sort handles) visible even
under `tableOnly`. `suppressPlatformFilter` drops the redundant
Platform chip since the platform is already fixed by the owning page,
so the user never sees a removable lock.
- StorageProps gains `showFilterToolbar`. Same idea for the canonical
StoragePageControls (search + status + group-by + sort + node filter
+ view).
Platform pages now mount their embedded surfaces with
`tableOnly + showFilterToolbar` (plus `suppressPlatformFilter` for
WorkloadsSurface):
- Docker > Containers
- Kubernetes > Pods
- TrueNAS > Storage, Apps
- vSphere > VMs, Storage
UnifiedResourceTable-backed sub-tabs (Docker Hosts, K8s Clusters/
Nodes/Deployments, vSphere Hosts) still rely on the table's built-in
sort handles only; a follow-up shared `PlatformInfraControls` row with
search + counters is the next operator-controls bridge.
Browser verification (Playwright, chromium, against live mock-mode
Pulse dev runtime):
- 9 tests, all pass. New assertion confirms that every embedded
Workloads/Storage sub-tab on a platform page now renders the
canonical search input (proving the v5-style operator toolbar is
back).
Targeted vitest:
- WorkloadsSurface.performance.contract.test.tsx adds a platform-page
embed contract assertion (37 tests total, all pass).
- Storage.test.tsx adds the matching StorageProps assertion
(39 tests total, all pass).
Contracts updated:
- performance-and-scalability.md Shared Boundaries: documents the
`showFilterToolbar` + `suppressPlatformFilter` platform-page contract
on WorkloadsSurface.
- storage-recovery.md Shared Boundaries: documents the
`showFilterToolbar` platform-page contract on StorageSurface.
Audit pass against the live mock backend (PULSE_MOCK_MODE=true) revealed
three platform sub-tabs that could never populate from canonical
resources today, even with a mature fixture environment:
- Docker > Swarm services: no `docker-service` resource is emitted by
the unified resource adapter at the /api/resources boundary.
- Kubernetes > Services: no `k8s-service` resource type is defined in
internal/unifiedresources; only k8s-cluster, k8s-node, pod, and
k8s-deployment are projected.
- TrueNAS > Hosts: TrueNAS connections produce storage, datasets,
physical disks, and app-containers but no top-level `agent` row
tagged with the `truenas` platform.
Per the platform-pages goal ("if a platform lacks enough canonical data
model support to make a useful page even after fixture work, do not
invent speculative UI — report the data-model gap and skip or gate
that surface deliberately"), the three sub-tabs are removed from the
page navigation and the resource queries are tightened to drop the
unused types. TrueNAS now defaults to /truenas/storage as the first
canonical operator entry point.
Kubernetes Nodes now also includes agent rows whose `sources` array
reports `kubernetes`, because the backend registry merges Pulse-Agent-
linked Kubernetes nodes onto the linked agent row. The merged agents
are the canonical projection of K8s nodes when an agent is installed
on them; treating them as Nodes makes the tab populate against mock
mode and live agent fleets alike.
Browser verification (Playwright, chromium, against live mock-mode
Pulse dev runtime):
- 8 tests, all pass. Every declared platform sub-tab now asserts at
least one canonical row populates under mock mode:
- docker: /docker/overview (2 hosts), /docker/containers
- kubernetes: /kubernetes/overview (1 cluster),
/kubernetes/nodes (3 merged agents), /kubernetes/pods (10),
/kubernetes/deployments (4)
- truenas: /truenas/storage (9), /truenas/apps (5)
- vmware: /vmware/overview (4 ESXi hosts), /vmware/vms (8),
/vmware/storage (datastores)
Targeted tests:
- dockerPageModel, kubernetesPageModel, truenasPageModel suites
updated and passing.
- resourceLinks.test.ts asserts the new TrueNAS default sub-tab.
Contracts updated:
- unified-resources.md Extension Points: platform default sub-tab must
land on a canonical surface that actually populates.
- storage-recovery.md Shared Boundaries: same; calls out TrueNAS
defaulting to /truenas/storage today.
Remaining canonical data-model gaps (intentionally not surfaced as
empty platform sub-tabs in the UI):
- internal/unifiedresources does not emit `docker-service` resources
even though mock.generateDockerServicesAndTasks() generates them in
the StateSnapshot; revisit when adding a docker-service projection.
- internal/unifiedresources does not define ResourceTypeK8sService;
revisit when k8s-service rows are added to the canonical model.
- internal/unifiedresources does not project a top-level TrueNAS
system as an `agent` row; revisit when the TrueNAS adapter promotes
the connection target into a canonical infrastructure row.
The unified resource projection returned by /api/resources leaves
`platformType` empty on several canonical resource types (storage,
agent, pbs, app-container, vm, k8s-deployment, pod, etc.) under the
mock fixture path and parts of the live backend. Platform-first pages
were filtering on `resource.platformType` directly, so Docker /
Kubernetes / TrueNAS / vSphere pages rendered empty under mock mode
even though the resources existed and were tagged with the right
`sources` array (['docker'], ['kubernetes'], ['truenas'], ['vmware']).
Introduce `resolveResourcePlatformType(resource)` in
`frontend-modern/src/utils/sourcePlatforms.ts` as the canonical reader
for "what platform family does this unified resource belong to". It
prefers `resource.platformType` when present and falls back to the
resource's `sources` array via the existing
`resolvePlatformTypeFromSources` normalization, so client-side family
grouping behaves identically against mock fixtures and live backends.
Each platform page model now buckets resources through that helper:
- dockerPageModel.ts
- kubernetesPageModel.ts
- truenasPageModel.ts
- vmwarePageModel.ts
Browser verification (Playwright, chromium, against live mock-mode
Pulse dev runtime):
- 4 no-data tests (stubbed empty /api/resources): all 4 pages render
sub-tab chrome and surface empty state.
- 4 populated tests (live mock backend): each platform asserts at
least one canonical row renders on its data-bearing sub-tabs:
- docker: /docker/overview (Hosts), /docker/containers
- kubernetes: /kubernetes/pods, /kubernetes/deployments
- truenas: /truenas/storage
- vmware: /vmware/vms, /vmware/storage
All 8 tests pass.
Verification artifacts staged:
- sourcePlatforms.test.ts extended with resolveResourcePlatformType
cases (19 tests pass).
- 68-platform-pages-shell.spec.ts extended with populated-state
assertions per platform.
Contracts updated:
- unified-resources.md Shared Boundaries: resolveResourcePlatformType
is the canonical reader for unified-resource platform family.
- frontend-primitives.md Extension Points: same.
Remaining mock fixture gaps (intentionally not asserted populated in
this commit; tracked for fixture extension):
- docker/services: default mock fixtures do not expose docker-service
resources at the /api/resources boundary.
- kubernetes/overview, kubernetes/nodes, kubernetes/services:
KubernetesClusters are generated but k8s-cluster, k8s-node, and
k8s-service resource projections are not surfaced.
- truenas/overview, truenas/apps: no TrueNAS agent or TrueNAS-scoped
app-container resources in default fixtures.
- vmware/overview: no VMware ESXi-host agent resources in default
fixtures.
Complete the platform-first navigation shift started in cdaeb3b84.
Primary navigation now enumerates exactly the supported platform
families (Proxmox, Docker, Kubernetes, TrueNAS, vSphere) and removes
Infrastructure, Workloads, Storage, and Recovery as equal top-level
tabs. Their tables continue to render inside each platform page via
the embedded tableOnly canonical surfaces; their routes remain wired
in App.tsx for deep-link compatibility, but they are not duplicated
in primary nav.
First-run UX decision: all supported platform tabs are alwaysShow:true
so first-run operators can discover what Pulse monitors. Unconnected
platforms render in a disabled tone (enabled/live derive from
canonical resource presence in state.resources), and the empty-state
inside the platform page itself surfaces the setup affordance. This
favors discoverability over hiding-until-connected, and is the
canonical decision recorded in the contract delta below.
Routing follow-up:
- routePreload.ts adds ROUTE_PRELOADERS entries for Docker, Kubernetes,
TrueNAS, and vSphere so platform navigation stays warm on first paint.
- mobileNavBarModel.ts MOBILE_NAV_PLATFORM_PRIORITY mirrors the new
platform-first primary set; legacy entries are removed.
Contracts updated:
- cloud-paid.md: records the platform-first primary-nav decision and
the alwaysShow + presence-derived enabled/live contract; explicitly
forbids reintroducing infra/workloads/storage/recovery as equal
primary tabs without a governed contract decision.
- frontend-primitives.md: PlatformTab and MOBILE_NAV_PLATFORM_PRIORITY
must mirror the supported-platform set; legacy entries intentionally
absent.
- performance-and-scalability.md: every supported platform must be in
the app-shell route preload registry; presentation-only platforms
must not be registered.
- ai-runtime.md: demotion does not affect Patrol or Assistant
addressability; platform pages must not replicate Patrol findings,
Assistant prompts, or AI launcher affordances inside their chrome.
Verification:
- App.architecture.test.ts extended to assert the platform-first
AppLayout structure (no infrastructure/workloads tabs, no
buildStorageRecoveryTabSpecs call) and the new routePreload entries.
- MobileNavBar.test.tsx extended to assert the platform-first
MOBILE_NAV_PLATFORM_PRIORITY ordering and the absence of legacy IDs.
- 68-platform-pages-shell.spec.ts (Playwright, chromium) re-run
post-demotion: all 4 platform pages still render with sub-tab chrome
against the live Pulse dev runtime in empty-resource state, proving
the first-run / no-data nav stays discoverable.
- vitest sweep across src/features, src/__tests__,
src/routing/__tests__ (351 tests) green. Pre-existing
PageControls.guardrails failure is unrelated (verified by stashing).
Unrelated dirty files (other-agent work in internal/ai/, status.json,
ai-runtime.md further edits) intentionally left untouched.
Introduce a shared platform-page primitive and four new top-level family
pages that mirror the v5-style Proxmox surface: chrome only, embedding
the canonical WorkloadsSurface, StorageSurface, RecoverySurface, and
UnifiedResourceTable in tableOnly/embedded mode with forced platform or
source filters. No dashboard cards, no fake data, no bespoke per-family
tables.
Pages added:
- /docker Hosts / Containers / Swarm services
- /kubernetes Clusters / Nodes / Pods / Deployments / Services
- /truenas Hosts / Storage / Apps
- /vmware Hosts / VMs / Storage (vSphere, first-lab-ready)
Top-level navigation entries are gated on platform presence in
state.resources, so empty platforms stay hidden by default; Proxmox
remains alwaysShow. Infrastructure, Workloads, Storage, and Recovery
remain available unchanged. Routing, navigation tab IDs, and the
document-title map are extended to match. Route preload and mobile-nav
priority changes are deliberately deferred to a follow-up commit to
avoid wider entanglement with parallel-agent edits to those shared
shell files.
Contracts extended for the new platform-page boundary:
- cloud-paid.md
- unified-resources.md
- storage-recovery.md
- ai-runtime.md
- frontend-primitives.md
Tests:
- dockerPageModel, kubernetesPageModel, truenasPageModel, vmwarePageModel
vitest suites (12 new tests).
- resourceLinks.test.ts extended for the new platform path builders.
- App.architecture.test.ts extended for the new lazy imports and routes.
- 68-platform-pages-shell.spec.ts Playwright smoke covering all four
pages and every sub-tab link against the live Pulse dev runtime.
- App.architecture, proxmox model, and Workloads suites continue to pass.
Skipped (canonical model not ready):
- Unraid as a top-level page: an agentHostProfile, no platform
projections; already surfaces through the Pulse-managed Hosts / Storage
views.
- Synology DSM, Microsoft Hyper-V, AWS, Azure, GCP:
governanceState=presentation-only, no canonical projections.
- add a forced run action to pulse_discovery for known resources
- make discovery progress describe model-backed evidence analysis rather than a live Assistant chat
- keep shared select hydration stable for persisted discovery intervals