Commit Graph

6273 Commits

Author SHA1 Message Date
rcourtman dcd59bf11b Fix Proxmox node version display
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.
2026-05-17 13:59:37 +01:00
rcourtman b746838b97 Collapse Infrastructure status to a single rolled-up signal
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.
2026-05-17 13:43:37 +01:00
rcourtman 702fc5aeb5 Stabilize hot-dev backend startup recovery 2026-05-17 09:54:11 +01:00
rcourtman 829d43a0d2 Share platform table filter state 2026-05-17 09:29:10 +01:00
rcourtman e803e823b7 Share platform table toolbar 2026-05-17 09:17:17 +01:00
rcourtman 7f10b0473a Share Proxmox platform page chrome 2026-05-17 09:06:45 +01:00
rcourtman 9ca54f79de Tighten TrueNAS overview storage section 2026-05-17 09:00:38 +01:00
rcourtman 6ef06e48ef Unify platform overview table layout 2026-05-16 23:41:22 +01:00
rcourtman e6bb190b98 Align Proxmox hosts table styling 2026-05-16 23:17:29 +01:00
rcourtman dae4fb3bb4 proxmox(overview): share the bars/sparklines toggle between the hosts table and the workloads
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.
2026-05-16 22:22:29 +01:00
rcourtman dc7c83a388 proxmox(nodes-table): drop the per-table search and status chip strip
v5's NodeSummaryTable rendered the rows directly without a search box or
status filter — node lists are typically short enough that visual scan
is faster than typing, and the Workloads filter below already covers the
place where filtering actually matters. Strip the SearchInput +
FilterButtonGroup + visible/total summary line so the table sits flush
under its section header, matching v5 more faithfully.

Removes the search/status signals, the filterPlatformResources call,
and the now-unused FilterButtonGroup / SearchInput /
PlatformResourceStatusFilter / FilterOption imports. The empty-state
fallback for "no nodes at all" stays; the "no nodes match current
filters" fallback is gone with the filters that produced it.
2026-05-16 21:44:51 +01:00
rcourtman 73a9608cc3 workloads(surface): compactGroupHeaders flag strips host stats from group rows
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.
2026-05-16 21:35:46 +01:00
rcourtman 17daa16eb4 proxmox(overview): restore grouped guest list to match v5 default
Drop the forced `flat` grouping I introduced one commit ago — v5 Dashboard
defaulted to grouped with NodeGroupHeader rows between the per-node
guest blocks, and only flipped to flat when the user toggled the
grouping control in the filter toolbar. Removing `forcedGroupingMode`
lets the user's persisted preference apply (default: grouped), matching
the v5 behavior exactly. The grouping toggle in the WorkloadsFilter view
options keeps working both ways.
2026-05-16 21:21:03 +01:00
rcourtman 7ae27fa044 proxmox(overview): drop duplicate status column, flatten guest list
Two duplication issues on /proxmox/overview after the v5-style hosts table
landed:

1. The nodes table rendered the status both as a colored dot beside the
   node name and as a separate Status badge column. The dot already
   conveys it; drop the Status column.
2. The embedded WorkloadsSurface was forced into `grouped` mode, which
   re-rendered NodeGroupHeader rows for the same hosts the new nodes
   table now shows on top. Switch the forced grouping to `flat`; each
   guest row keeps its Node column so the per-row attribution is intact
   without the duplicated headers.
2026-05-16 21:16:36 +01:00
rcourtman 07f97c3f58 proxmox(nodes-table): metric bars, gauges, and badges for the canonical look
The first pass shipped the v5 layout but the table itself was raw cells
(formatPercent text, plain digits). Match the canonical platform-page
metric primitives so CPU / Memory / Disk render as actual bars with
severity coloring, temperature uses the shared gauge, and status / VM /
container / cluster columns read as proper badges.

- CPU: ResponsiveMetricCell (animated bar + sparkline overlay,
  severity-aware text color).
- Memory: StackedMemoryBar (handles totals + percentOnly fallback).
- Disk: StackedDiskBar (per-disk segments when available, otherwise the
  aggregate disk row).
- Temperature: TemperatureGauge (severity-colored °C).
- Status: rounded-full pill via getStatusIndicatorBadgeToneClasses.
- Version: mono pill on bg-surface-alt.
- Uptime: warns in orange when < 1h (v5 "recently restarted" cue).
- VMs / CTs counts: sky / violet pill badges, muted when zero.
- Cluster: subtle chip on bg-surface-alt.

Online-only metric cells fall back to a muted em-dash when the node is
degraded or offline, matching how the canonical host table elsewhere on
the platform pages handles non-running hosts.
2026-05-16 21:05:50 +01:00
rcourtman a64c8c33e9 proxmox(overview): restore v5-style hosts table above the guest list
v5 Dashboard rendered Proxmox node summary on top, the search/filter
toolbar in the middle, and the guests table at the bottom; the v6 Proxmox
overview tab dropped to "workloads only" during the platform-first
migration. Operators lost the at-a-glance node-level uptime, load, and
temperature view above the guest rows.

Add ProxmoxNodesTable mirroring the canonical Docker/K8s/TrueNAS/vSphere
host-table shape — Node / Version / Uptime / CPU / Memory / Disk / Temp /
VMs / CTs / Cluster, with self-contained search + status filter — and
render it above the embedded WorkloadsSurface on /proxmox/overview. The
existing Workloads filter toolbar and grouped guest table sit below it
unchanged.
2026-05-16 20:54:04 +01:00
rcourtman edc8bd5df0 frontend(platform-first): drop alerts/findings/drawer cross-jump chips and dead resourceLinks helpers
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.
2026-05-16 20:42:47 +01:00
rcourtman abb6f86aeb frontend(platform-first): retire legacy top-level routes and cross-link affordances
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.
2026-05-16 18:57:37 +01:00
rcourtman e66cfc4991 proxmox(mail-drawer): canonical metric palette across all bars
Mail Gateway drawer was still on raw bg-emerald-500 / bg-amber-500 /
bg-red-500 / bg-blue-500 / bg-purple-500 — bright full-saturation
colors that visually clashed with the muted bg-metric-*-bg tokens
(rgba 0.6 opacity) the rest of the app uses on its bars.

Where a segment is severity-meaningful, switch to the canonical
metric tokens:
  - Inbound: Clean=normal, Spam=warning, Virus=critical
  - Quarantine: Spam=warning, Virus=critical
  - Spam histogram: 0=normal, mid=warning, high=critical

Where a segment is categorical-only (no severity meaning), keep a
distinct color but at /60 opacity so it sits at the same brightness
as the metric palette:
  - Bounces/Attachment → orange-500/60
  - Greylist → blue-500/60
  - In direction → blue-500/60
  - Out direction → purple-500/60
  - RBL → purple-500/60
  - Pregreet/Blacklisted → slate-500/60

End result: every bar in the drawer matches the visual weight of
the Workloads CPU/Memory/Disk bars.
2026-05-16 17:12:41 +01:00
rcourtman e51a9f083f proxmox(ceph-drawer): use canonical metric color tokens on capacity bars
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).
2026-05-16 17:07:51 +01:00
rcourtman 8add51358b proxmox(drawers): write bar values on the fill, not in a key below
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.
2026-05-16 17:05:15 +01:00
rcourtman 035b39ba04 proxmox(drawers): drop stat-card tiles for diagrams + tables
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.
2026-05-16 16:56:50 +01:00
rcourtman a26dad0607 proxmox(mail): bespoke drawer with cluster nodes + queues + stats
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).
2026-05-16 16:47:20 +01:00
rcourtman 21e679e45c proxmox(ceph-drawer): stop content overflowing tiles and totals strip
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.
2026-05-16 16:34:20 +01:00
rcourtman 455b789428 proxmox(ceph): inline drawer with per-pool and per-service detail
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).
2026-05-16 16:30:24 +01:00
rcourtman ff8b7e79f3 hooks(resources): project ceph meta onto Resource.ceph
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.
2026-05-16 15:08:04 +01:00
rcourtman f8f39d3e18 vmware(storage): bespoke datastores table replaces filtered Storage view
/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).
2026-05-16 15:01:44 +01:00
rcourtman 5ca05949b8 workloads(drawer): seed guest history charts with current values
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.
2026-05-16 14:56:57 +01:00
rcourtman 95171ff7bc proxmox(ceph): bespoke cluster table replaces filtered Storage view
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.
2026-05-16 14:35:22 +01:00
rcourtman 7b821c9cfa mock(pve-backups): seed recent backup-task history
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.
2026-05-16 14:15:53 +01:00
rcourtman 27f35d9819 proxmox(replication+backups): bespoke tables backed by new read-only endpoints
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.
2026-05-16 13:57:53 +01:00
rcourtman 0aa4317532 platforms: drop unsupported type tokens from Proxmox / TrueNAS / vSphere queries
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.
2026-05-16 13:29:35 +01:00
rcourtman 3ecc3fd0d1 proxmox(mail): bespoke Mail Gateway table; project uptime on PBS + PMG
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 (c7bdd11e05b94724bf0fcf9944b72dc2930b1e03c77f6). 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.
2026-05-16 13:22:48 +01:00
rcourtman 1e03c77f6f proxmox: bring sub-tabs onto the showFilterToolbar contract
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.
2026-05-16 13:13:35 +01:00
rcourtman 72dc2930b7 k8s(nodes): bespoke nodes table with Kubelet/runtime/capacity columns
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 (c7bdd11e05b94724bf0fcf9944b). 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.
2026-05-16 13:10:04 +01:00
rcourtman 0fcf9944b3 docker + vsphere: bespoke host tables; project uptime/temp on docker-host adapter
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 (c7bdd11e05b94724bf69f70a3fc). 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.
2026-05-16 13:02:06 +01:00
rcourtman 5b94724bf2 truenas: bespoke Systems + Disks tables; project uptime/temp on the system row
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.
2026-05-16 12:51:36 +01:00
rcourtman c7bdd11e04 platforms: full column-fit audit — project uptime/temp, native tables for Docker Services and K8s Clusters
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).
2026-05-16 12:36:57 +01:00
rcourtman 69f70a3fcd k8s(deployments): replace generic infra table with a deployment-native table
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.
2026-05-16 12:23:57 +01:00
rcourtman 60e649a456 k8s(deployments): also synthesize DiskRead/DiskWrite rates so the I/O columns populate
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.
2026-05-16 12:11:29 +01:00
rcourtman 8fb141c0a4 k8s(deployments): synthesize deployment metrics in mock mode so the table stops showing dashes
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).
2026-05-16 12:05:23 +01:00
rcourtman 7938f28de4 platforms: scale K8s clusters to 3 + fix VMware storage source matching
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.
2026-05-16 11:43:41 +01:00
rcourtman 294ac1da04 platforms: close remaining gaps — Swarm services, vSphere fixtures, TrueNAS systems, source-filter suppression
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.
2026-05-16 08:35:44 +01:00
rcourtman cef057943a mock(fixtures): scale default fixture sizes to a mature SMB homelab
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.
2026-05-16 08:16:00 +01:00
rcourtman 469691bc74 frontend(platforms): add v5-style operator toolbar to UnifiedResourceTable platform sub-tabs
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.
2026-05-16 00:08:12 +01:00
rcourtman 65b069dbba frontend(platforms): restore v5-style operator controls on embedded canonical surfaces
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.
2026-05-16 00:02:16 +01:00
rcourtman f81490ca4f Keep discovery manual refresh visible 2026-05-15 23:55:22 +01:00
rcourtman b0c4faa4a0 frontend(platforms): drop platform sub-tabs whose canonical projection is missing
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.
2026-05-15 23:49:23 +01:00
rcourtman cfc3fd1606 frontend(platforms): resolve resource platform family via sources fallback
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.
2026-05-15 23:38:40 +01:00
rcourtman 976e7c6b42 Add settings discovery refresh action
- expose a manual discovery sweep API

- wire Assistant & Patrol settings to run new, changed, and stale workload refreshes
2026-05-15 23:27:08 +01:00