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.
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