Switching to List view used to light up the Reset/Clear all button
even though no chip rendered for it, conflating a view-option
preference with active filter state. Grouped/List is now treated like
the other view options (bars/trends, charts toggle, columns): no
contribution to Clear all visibility, no reset by Clear all.
Extracts the per-disk Storage card introduced for the Docker host drawer
into a shared `DrawerDiskListCard` under `components/Workloads/` and
threads an optional `disks` prop through `NodeDrawer` →
`NodeDrawerOverview`. The Docker drawer now imports the shared component
instead of carrying its own copy, and `NodeDrawerOverview` renders the
shared card whenever the caller supplies two or more disks (falling back
to the existing aggregate Storage rows otherwise).
`ProxmoxNodesTable` is left untouched in this commit because the file is
currently dirty under another agent's lane; wiring the `disks` prop on
its `<NodeDrawer>` call site is a separate one-line follow-up once that
lane settles.
Pre-push tsc rejected the discovery test fixtures because ResourceDiscovery
has more required fields than the minimum needed to exercise the new
identified-service helper. Widen the vi.fn return type so
mockResolvedValueOnce accepts partial-record fixtures, and cast the
fixtures through `unknown as ResourceDiscovery` (matching the existing
pattern used for the ports/facts subobjects in the same suites).
Discovery records carry rich identification (service name, category,
ports, cli access, paths) but until now they only rendered inside the
Discovery sub-tab. Operators opening a workload drawer to look at memory
or backups had no signal that Pulse already knows the LXC is "Homepage
Dashboard" or the Docker container is "Proxmox Backup Server" — they
had to switch tabs to see it.
Add an "Identified Service" card to GuestDrawerOverview that shows the
service name, category, port count, CLI access hint, and confidence
percent. The card mirrors the existing System/Guest Info/Backup card
shapes so it slots into the same flex grid.
useGuestDrawerState passively loads the discovery record via getDiscovery
(no scan triggered — manual scans, progress UI, and approval prompts
stay owned by DiscoveryTab and useDiscoveryTabState). A new presentation
helper, getDiscoveryIdentifiedSummary, packages the record into the
display shape and returns null when the record has no meaningful
identification — same gate as the Discovery tab's hasValidDiscovery, so
both surfaces render or hide together.
Contracts: amend frontend-primitives Completion Obligations to fence the
new identified-service reducer to discoveryPresentation.ts, and amend
performance-and-scalability Current State to require that out-of-tab
discovery surfacing stay passive (no scans). Verification via
discoveryPresentation.test.ts and GuestDrawer.test.tsx covers the
populated/empty/null branches and the drawer card render.
Workloads table row surfacing is the natural next step but blocked
behind the parallel agent's in-progress WorkloadPanel/WorkloadsSurface/
useWorkloadsState changes; revisit after those land.
For app-container workloads, resolveDiscoveryTargetForWorkload was
populating the discovery resourceId with guest.id — the frontend's
synthetic workload hash (e.g. "app-container-ed79032e5852577f"). The
backend wraps that as docker exec <id> sh -c '...', the Docker daemon
has no container by that name, so every scan came back with empty
stdout and the AI returned "Unknown Infrastructure Resource" at
confidence 0. To the user the Discovery sub-tab looked empty after a
"successful" run.
guest.containerId is already populated from resource.docker.containerId
for Docker-managed app-containers (useWorkloads.ts:450). Use it as the
discovery resourceId so the agent can actually docker exec into the
right container. VM and system-container branches were already correct
because they use vmid for pct exec / qm guest exec.
Fix the existing workloadTopology test that locked in the broken
behavior, and add containerId to the workloads.test.ts fixture that
exercises the same resolver branch.
The backend has had DiscoveryModel + GetDiscoveryModel() with the
shared-default fallback alongside ChatModel and PatrolModel, but the
settings UI only surfaced Assistant and Patrol overrides. Discovery
silently used whatever the shared default was set to, with no way to
dial it independently — yet Discovery is the surface where a cheaper
model (e.g. Haiku) is most often the right tradeoff.
Thread discoveryModel through the form state, load, save payload, and
failure analyzer, and render a third picker in the Model Overrides
section mirroring the Assistant/Patrol pattern. Generalize the section
header and shared-default copy so they no longer enumerate surfaces.
Extend the frontend-primitives contract to recognize Discovery alongside
Assistant and Patrol as a per-surface override, and lock the picker
presence into settingsArchitecture.test.ts.
The disk column was sized for the old "[bar] N% (used/total)" content
(~150-180px). With vertical micro-bars it only needs ~80-110px, so
the disk weight drops from 15 to 7 on desktop/compact (and the
proportional weights drop on mobile/tablet). Freed space redistributes
to Node/CPU/Memory/Cluster which all have more content to show.
Each vertical bar also gets a cursor-help cursor, a subtle ring on
hover, and a brightness/scale tick so users discover that bars are
individually inspectable — the native browser title tooltip on each
bar (disk name + percent + sizes) was already wired; this just makes
it visually discoverable.
Adds a 'vertical-bars' mode to StackedDiskBar and opts the hosts
table into it when a host has more than one disk. Each disk renders
as one 5px-wide vertical bar (fill height = utilization, color =
threshold), grouped and centred in the cell. All disks get identical
visual weight, no single disk is named or privileged, and 1-10 disks
all render the same way. Hover keeps the full per-disk breakdown.
Single-disk hosts still go through the original single-bar path.
The narrow-viewport card was rendering the row's status badge, problem
line, and Manage button as siblings inside a single flex-justify-between
row in the card footer. A long problem string ("Agent offline · 4h
ago", "Credentials invalid · 14 days") would squeeze the layout
unpredictably and push the Manage button into wrap territory.
Pull the problem line out of the flex row and onto its own line
beneath the status/Manage row, matching the wide-table treatment.
The SOURCE and ENDPOINT columns each carried a single static fact per
row (which collection method, which URL) that didn't help the user
scan for problems and that pushed the three columns the user actually
needed (System, Coverage, Status) into narrower lanes.
Fold source and endpoint into the System cell as a secondary line
beneath the name and subtitle: a small source badge inline with the
truncated endpoint URL. The cluster row, which has no endpoint, just
shows the source badge. Member rows follow the same pattern under
their indent.
New widths (System / Coverage / Status / Actions): 36 / 32 / 24 /
7rem at standard width, 34 / 34 / 22 / 7rem at xl. Section group
headers and lastError/empty-state rows colspan from 6/5 to 4/3.
Card layout (narrow viewport) is unchanged — it already stacks fields
vertically, not as table columns.
The agent-offline detail line was being produced by synthesizing a fake
FleetGovernanceSignal with key 'agent-attachment' and laundering it
through visibleFleetGovernanceSignals so the chip filter would pick it
up. With the chip stack gone and the row exposing a single
InfrastructureRowProblem, the synthesis is dead weight — it pretended
to be server-emitted governance state when it was really derived from
the agent's connection state.
- Replace agentAttachmentSignal with agentAttachmentProblem, returning
InfrastructureRowProblem directly. The row builder applies it ahead
of the fleet-highlights problem, so an offline agent always wins over
a downstream credential/config drift signal on the same row.
- Remove 'agent-attachment' from FleetGovernanceSignalKey now that no
signal carries it.
- Simplify the setup-summary Live/Needs attention counters to read off
the user-visible row state (statusLabel, problem) instead of
re-deriving via signal predicates. The previous path could
in principle disagree with what the row actually rendered.
- Drop rowFleetSignals, rowHasFleetTone, rowHasVisibleFleetTone — all
now unused.
Each platform overview page (Docker, Kubernetes, TrueNAS, vSphere)
composed its own <TableCardHeader title="Hosts" /> etc. independently,
so dropping the redundant title strips on Proxmox in the previous
commit didn't propagate. The architecturally correct fix is at the
primitive: the header strip only earns the chrome when it has real
content to show.
- TableCardHeader now renders null unless there's an `actions` slot or
an active `showClearAction`. Title-only is a silent no-op — every
platform-table caller stops showing a vestigial label automatically.
- `title` becomes optional on the props type so callers that intend to
use the strip purely for an action button (no caption) don't need to
pass a dummy.
- Title still renders when paired with actions, which keeps
StorageContentCard's "STORAGE / Storage|Physical Disks" header and
RecoveryProtectedInventorySection's "Protection coverage / Recovery
events" back-button strip working as before.
Net effect: Docker, Kubernetes, TrueNAS, vSphere overviews now match
Proxmox without touching any per-platform file, and future platform
tables can't accidentally re-introduce the strip by typing
title="Hosts".
TypeScript can't narrow FleetGovernanceSignal.tone through the find
predicates, so the helper now returns the literal tone for each branch
instead of forwarding the wider signal tone.
The Status column was rendering a stack of coloured chips drawn from
~ten independent fleet-governance enums, plus a synthetic "Fleet OK"
when none of them tripped. Source badge, status badge and chip array
could each be correct in isolation while telling competing stories on
the same row ("API + Agent" / "Active" / "Agent offline · 4h" all at
once). The canonical fix is to let the Status column tell one story.
- Add an `InfrastructureRowProblem` model field that names the single
most-important problem on a row in plain English, derived from the
prioritised fleet highlights via a new `primaryRowProblem` helper.
- Render that problem as a small italic detail line beneath the status
badge instead of as a parallel coloured chip. Critical problems read
rose, warnings amber; absence means nothing is wrong.
- Drop every `<For each={fleetHighlights}>` chip stack in the table —
four call sites across the row, member, and narrow-card layouts.
- Drop the "Install agent" nudge on non-PVE systems. PBS/PMG/TrueNAS/
vSphere are fully covered by their own API; the agent only adds host
telemetry, which is rarely the point of monitoring a backup server.
Scope the "Needs agent" counter the same way so the header summary
stops asking for an agent that isn't needed.
The fleet highlight signals stay on the row model for downstream
consumers (Manage drawer, audit views, tests) — only the parallel chip
render is gone.
- Move Bars/Sparklines toggle (and its sparkline-range sub-control) out
of the WorkloadsTable card header into the WorkloadsFilter's
viewOptionsTrailing, alongside Grouped/List, Charts, and Columns.
It's a view-mode control, not a per-row filter, and grouping it with
the other view options matches the FilterBar chip pattern.
- Drop the "Hosts" and "Workloads" title strips entirely from
ProxmoxNodesTable and WorkloadsTable. The column headers already
identify the tables (NODE/VERSION/UPTIME vs NAME/TYPE/INFO), the
FilterBar sits between them as a strong visual separator, and the
page header / route already names what the user is looking at.
- Relocate the pinned-summary "Clear" button (previously rendered as
an action in the now-removed WorkloadsTable header) into the
WorkloadsFilter viewOptionsTrailing, rendered conditionally via
pinnedSelectionActive() / onClearPinnedSelection.
- Tighten TABLE_CARD_HEADER_CLASS vertical padding (py-2 -> py-1) so
the remaining card-header strips (other platform tables, Recovery,
Storage) stop eating a wasted row of space.
Net effect on the Proxmox overview: Hosts and Workloads cards each
shed ~25-30px of header chrome. Same shared metricDisplayMode signal
drives both tables, same clearPinnedSummaryScope clears the same pin.
When a Pulse Agent attached to a Proxmox system goes silent, the row
kept advertising "API + Agent" while the chip below said "Agent offline".
Three signals on one row, two contradicting each other.
- Source badge now reflects what is currently contributing data, not what
has been configured. A connection only counts if its state is active
(or paused). When the agent goes silent, the badge collapses to "API"
and the existing "Agent offline · Xh ago" chip explains why.
- Cluster member rows always have an implicit API path via the cluster's
primary, so they now resolve to "API + Agent" when their paired agent
is contributing — previously they showed "Agent" only.
- Drop the duplicated standalone-agent row whose hostname matches a
cluster member elsewhere in the table. Same physical box, one row.
Read canonical Proxmox pveVersion metadata for host rows and share compact PVE version formatting across the Proxmox page and node headers. Add contract and proof coverage for the new Proxmox metadata fields.
The Connected systems table stacked four chips per row (Stale, Adapter
degraded, Remote control disabled, Fleet OK) drawn from independent fleet
enums, making it hard to see whether a system actually needs attention.
- Drop liveness chips from the highlight list; the row's status badge
already shows that state and was duplicating it.
- Drop the adapter chip when liveness is non-OK, since adapter health
degrades automatically once an agent stops heartbeating.
- Drop the info-tone "Remote control disabled/enabled" chip; default
policy state is not a posture warning and belongs in the Manage drawer.
- Stop synthesizing "Fleet OK" when nothing is wrong; an empty highlight
list paired with a green status badge says it more cleanly.
- Roll up the row state across attached components (not just cluster
members), so a stale paired agent surfaces on the parent row instead
of being hidden behind an Active PVE primary.
Before this commit the bars / sparklines segmented control inside the
Workloads filter only drove the guest table below. The new hosts table
on top still showed metric bars regardless of the toggle, which read as
half-wired UX once the user noticed.
Lift ownership of the metric display mode and the sparkline range to
ProxmoxPageSurface:
- useWorkloadsControlsState gains optional `metricDisplayMode`,
`onMetricDisplayModeChange`, `metricHistoryRange`, and
`onMetricHistoryRangeChange` options. When supplied, the controls
layer short-circuits to the page-provided accessor + change handler;
otherwise it falls back to the existing persistent signals, so
standalone Workloads usage is unaffected.
- WorkloadsSurface forwards those four overrides through
WorkloadsSurfaceProps so platform pages can opt in.
- ProxmoxPageSurface creates the two persistent signals at the page
level (same `STORAGE_KEYS.WORKLOADS_METRIC_DISPLAY_MODE` /
`STORAGE_KEYS.WORKLOADS_METRIC_HISTORY_RANGE` keys, so existing user
preference carries over) and passes the accessor + setter down to
both the embedded WorkloadsSurface and ProxmoxNodesTable.
- ProxmoxNodesTable accepts `metricDisplayMode` + `metricHistoryRange`
accessors. When the mode is `sparklines` it renders
MetricMiniSparkline for CPU / Memory / Disk using
`useWorkloadTableMetricHistory`; the cache key matches the
workloads-table reader so the two instances dedupe their fetches.
When the mode is `bars` it keeps the canonical
ResponsiveMetricCell / StackedMemoryBar / StackedDiskBar rendering
from the previous commit. The legacy Node shape that
`getNodeMetricSeries` keys on is projected from the canonical
Resource (id / name / instance / linkedAgentId).
A new contract test pins the override threading + the page-level wiring
so a future refactor can't silently fork the toggle state again.
performance-and-scalability contract Current-State documents the
shared-toggle model.
After landing the v5-style Proxmox nodes table above the embedded
WorkloadsSurface, the NodeGroupHeader rows kept rendering per-host CPU /
Memory / Disk / uptime / temperature / version inline with the workload
columns — exactly the stats the new top table already owns. The
information was correct twice, which is louder than necessary.
Add a `compactGroupHeaders` prop on WorkloadsSurface that flows through
useWorkloadsState → WorkloadsTable → WorkloadPanel. When set, the panel
calls NodeGroupHeader without `columns` / `renderColumnCell` and forces
`showFactsInName={false}`, so the group row falls back to the existing
single-cell colspan layout: status dot + linked node name + cluster
badge + agent badge. The per-row CPU / Memory / Disk metric cells in
the guest rows below are unchanged.
ProxmoxPageSurface sets `compactGroupHeaders` on its embedded surface.
performance-and-scalability contract gains a Current-State note covering
the new threading, and the WorkloadsSurface performance contract test
pins the prop threading + the panel branch so a future refactor can't
silently drop the compact mode.
Three surfaces still rendered "Open in Infrastructure / Workloads / Storage /
Recovery" chips that 404'd after the previous platform-first migration
commit; remove them and delete the now-orphaned resourceLinks helpers.
- AlertResourceIncidentsPanel no longer renders surface-link chips.
- FindingsPanel (Patrol) no longer renders surface-link chips; the
useResources lookup is gone with them.
- resourceDetailDrawerOperationalModel.buildRelatedLinks now returns only
service-detail links (PMG thresholds); cross-jump injection through
buildResourceSurfaceLinksForResource was retired.
- resourceLinks.ts: delete buildInfrastructureResourceHref /
buildInfrastructureResourceLink / buildInfrastructureHrefForWorkload /
buildWorkloadsHrefForResource / buildStorageHrefForResource /
buildRecoveryHrefForResource / buildResourceSurfaceLinksForResource /
buildResolvedResourceSurfaceLinks, the dedupeResourceSurfaceLinks /
resolveKubernetesContextForResource / resolveHostHintForResource /
resolveDockerWorkloadsHintForResource / hasMergedSource /
resolveStorageRouteSource helpers, and the ResourceSurfaceLink /
ResolvedResourceSurfaceLinkOptions types.
- Tests: resourceLinks.test.ts, FindingsPanel.links.test.tsx,
resourceDetailDrawerOperationalModel.test.ts, AlertResourceIncidentsPanel
.test.tsx, FindingsPanel.test.ts, Alerts.helpers.test.ts,
frontendResourceTypeBoundaries.test.ts updated to assert the removal
contract instead of the old link-rendering contract.
- Subsystem contracts (alerts, patrol-intelligence, frontend-primitives,
storage-recovery, unified-resources) gain Current-State notes
documenting the chip-strip removal and the helper deletion so future
work doesn't resurrect the legacy cross-jump surface.
Primary nav moved to platform-first (Proxmox/Docker/K8s/TrueNAS/vSphere) and
the standalone Infrastructure/Workloads/Storage/Recovery/Ceph routes were no
longer reachable from the UI — they only persisted as deep-link fallbacks and
as cross-jump targets for command palette / shortcuts / drawer links.
Code:
- RuntimeHome and NotFound redirect to /proxmox/overview; kiosk-mode
redirect follows the same canonical home.
- App.tsx, routing/navigation.ts, routing/routePreload.ts, AppLayout.tsx
drop the legacy routes, preloaders, tab IDs, and tab-title entries.
- Old page wrappers (Infrastructure/Workloads/Storage/Recovery/Ceph) and
the orphan CephServiceIcon and workloadsLink helper are deleted; the
underlying feature surfaces (InfrastructurePageSurface, WorkloadsSurface,
StorageSurface, RecoverySurface, ProxmoxCephTable) stay embedded inside
platform pages.
- Command palette and keyboard shortcuts now expose the five platform
pages (g p/d/k/n/v) plus Patrol/Alerts/Settings; legacy g i/w/s/b are
retired.
- K8s Namespaces/Deployments drawer "view pods" buttons route to
/kubernetes/pods (the legacy ?context= + ?namespace= filter doesn't
carry forward yet — the new sub-tab doesn't consume those params).
- Workload-row node-name click, host-row workloads icon, resource
correlation/change drilldowns, and the PBS "Open Recovery Events" link
render as plain text or are dropped — platform pages already surface
the related data in adjacent sub-tabs.
Governance:
- Subsystem contracts updated to match: storage-recovery and
performance-and-scalability drop the retired page wrappers from their
Canonical Files lists; ai-runtime, cloud-paid, frontend-primitives, and
unified-resources gain Current-State notes describing the platform-first
redirect, palette/shortcut re-anchoring, and cross-link affordance
removal so future work doesn't resurrect the retired paths.
- status.json L8 (First-session & UX polish) evidence: the retired
pages/Infrastructure.tsx wrapper is replaced by pages/Proxmox.tsx as
the canonical post-redirect landing surface.
- registry.json: 18 retired-file entries removed from owned_files,
path-policy match_files, and exact_files across the route-shell-and-
operations, workload-presentation, storage-recovery, and unified-
resources subsystems. Surviving entries continue to cover the proof
set — the embedded feature surfaces remain canonical and the retired
page wrappers were thin shells with no remaining proof obligation.
- canonical_completion_guard_test.py and subsystem_lookup_test.py
retargeted: the recovery and storage product-surface fixtures now
exercise the canonical components/* surfaces instead of the retired
pages/* shells, matching the new registry shape.
Tests: typecheck clean. Vitest 5820 pass / 6 pre-existing fail
(PageControls.guardrails, useStoragePoolDetailModel, ApprovalSection x3,
frontendResourceTypeBoundaries:1018) — all failing on HEAD before this
change with clean porcelain on the offending source files. Python
canonical_completion_guard_test (141) and subsystem_lookup_test (183)
green.
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.
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.
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.
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.
- 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