The RBAC, audit-webhook, relay, and reporting settings panels each hand-
rolled the same Pro feature-gate shell (title + body + upgrade CTA) with
gratuitous divergence: h4 vs p headings, a bespoke relay button class, a
hardcoded label, and an icon only on relay. Extract one shared
FeatureGateSection that owns the gate's layout, heading semantics, and the
canonical upgrade button (getUpgradeActionButtonClass + UPGRADE_ACTION_LABEL
+ UpgradeLink), and route all four panels through it. Callers keep their
contextual container (divided SettingsPanel row, Card tone=info,
OperationsPanel) and their per-feature copy owners.
Behavior is unchanged: each panel passes its existing showUpgradePrompts /
upgradeDestination / copy, so the paid-runtime-blocked tone and presentation-
policy suppression are preserved. Repoint the upgrade-contract guardrail in
frontendResourceTypeBoundaries onto the shared component.
Keep Overview as the stable platform landing tab, hide workflow tabs without current inventory or signal, and route legacy Docker containers URLs back to the host-plus-workloads overview.
Adopt replacement managed hot-dev supervisors during listener waits and let mock-mode toggles continue only when the browser entrypoint proves the requested runtime state after a non-clean restart.
Make the public MSP signup path create immediate subscription checkout sessions instead of inheriting the individual Cloud trial helper. Preserve the existing individual Cloud trial path, mark checkout billing mode in metadata, and seed MSP checkout-created Stripe account mappings as active.
Remove ownership and proof-policy references to Proxmox backup presentation files that were retired by the guest-centric backup surface, keeping the storage-recovery contract aligned with tracked runtime paths.
MULTI_TENANT.md documented org/member/sharing management but never the MSP
workflow that ties it together: org per client, an org-bound agent:report
token that auto-routes each client's agents into their isolated org, optional
per-client node aliasing via --hostname/PULSE_HOSTNAME, optional split-port
agent ingest, and per-org alert routing to ConnectWise. Also clarifies that
one multi-tenant instance covers all client orgs (no per-client Pro license).
The capabilities already existed; this closes the discoverability gap two MSP
leads hit while evaluating.
The backups page was four navigation levels deep (Backups tab -> four
sub-tabs -> a three-way Source details split) fronted by per-day activity
bar charts and PBS-jargon summary strips. That buried the one question a
backup monitor exists to answer -- is every guest backed up, recently, and
did the last job run -- under a read-only re-render of the PBS/PVE console.
Collapse it to a single coverage table (one row per guest, posture dot,
latest restore point, per-source evidence in the row expansion) and:
- Add a Backup servers table: PBS reachability + datastore fill % (the
headline backup risk) + dedup, from data already on the pbs resource
(new ResourcePBSDatastore type). Filtered to type==='pbs' so a PBS
datastore storage row can't render as a phantom server.
- Split orphaned backups (records whose guest no longer exists in
inventory, keyed backup: not resource:) out of the main table into a
collapsed section, so live named guests aren't pushed below nameless
stale records. Health strip counts live guests only. Also surfaces
orphaned-backup detection (refs #1286).
- Colour posture/task words on the exception only (amber/red for
attention/danger, neutral when healthy), matching the Replication
status-word and datastore-usage tone patterns.
- Remove the now-unused source-detail tables, activity chart, and their
presentation helpers.
Verified on live PBS data: tsc clean, proxmox vitest green.
Mock and real mode shared tmp/dev-config, so toggling between them left
residue each way: real connections (and a live local agent) showed in
Settings during mock, and mock-cluster alerts lingered in real mode.
The toggle script had an isolation flag but it only reached the
standalone runtime, never managed-hot-dev.
hot-dev.sh now routes mock mode to tmp/mock-data and real mode to
tmp/dev-config, choosing the dir from the canonical PULSE_MOCK_MODE flag
that toggle-mock writes to tmp/dev-config/.env. The flag is read from
that file authoritatively rather than from the environment, because a
stale PULSE_MOCK_MODE exported into the hot-dev supervisor from a prior
mock run would otherwise keep real mode pinned to the mock dir.
toggle-mock status now reports the isolated mock dir for hot-dev
runtimes so it matches what the backend actually uses.
Verified a full round trip: real -> dev-config (real connections only),
mock -> mock-data (mock data only, no real bleed), real -> dev-config
(no mock-cluster residue). Trade-off: real metrics history no longer
accrues during mock UI sessions, which is the right call for clean
dev switching.
An unreachable PBS connection rendered its problem line as 'Rollout
blocked' — Pulse Agent rollout terminology that makes no sense for a
pull-based API source that simply can't be reached. The backend echoes
rollout/config governance state onto every connection, and because the
liveness signal is intentionally filtered out of the highlight list
(it duplicates the status badge), primaryRowProblem fell through to the
agent rollout signal.
Gate the agent-binary / managed-config governance signals (version
drift, config drift, staged rollout, binary updates, remote command
policy) on the connection actually running a Pulse Agent / collector
(agent, docker, kubernetes). Pull-based API sources (pve, pbs, pmg,
vmware, truenas, availability) now only carry source-agnostic posture
(enrollment, liveness, credential health, adapter), so an unreachable
PBS reflects its reachability instead of fictitious rollout state.
Verified against real infra: no API source shows rollout/config/version
language; the only problem lines are legitimate 'Agent offline'
attachment messages.
Also reconcile tests with two intentional UI changes from the prior
infrastructure-table commits that the pre-commit hook (lint only, no
vitest) did not catch:
- The row-level install-agent shortcut is now an icon button; name it
'Install agent' so it stays distinct from the group-level 'Install
Pulse Agent' action the tests assert against.
- The system name's OS/identity and cluster/member descriptors moved
into the name tooltip to keep rows single-line, so the workspace
tests assert them via title instead of visible text.
Pulse Agents push telemetry to Pulse, so there is no pause concept for
them: the backend hardcodes agent connections to Enabled=true with
SupportsPause=false, and the frontend setEnabled() throws
'Pause is not supported for agent connections' before any request is
sent. The agent Manage dialog still rendered an enabled 'Pause
connection' button (and an 'Enabled' status pill implying a toggle),
so the button could only ever surface that error. Remove both from the
agent panel. Pause stays for API-pulled sources (pve/pbs/pmg/vmware/
truenas), where it genuinely stops polling.
Remove source is unchanged and works: it calls DELETE
/api/agents/agent/{id}, which drops the host from state, revokes the
agent token, and blocklists the host so future reports are rejected
until 'Allow reconnect'. The dialog already explains the host keeps
running until uninstalled.
Also declutter the dialog the user flagged as 'weird':
- Drop the redundant summary header (name, 'Pulse Agent' eyebrow, OS,
endpoint) that duplicated the dialog title/subtitle and the info
cards below it.
- Clamp the uninstall command previews to 3 lines so the long Windows
PowerShell one-liner no longer dominates the modal; the Copy buttons
still deliver the full command.
The connected-systems table on /settings/infrastructure stacked three
lines into the System cell (name, OS/identity subtitle, source badge +
host) and let Coverage and the status problem line wrap, so every row
ran 57-75px and was hard to scan. Every other top-level Pulse table
(Proxmox overview, backups) is single-line, one datum per column.
Give each datum its own column: System | Method | Host | Coverage |
Status | Actions. The OS/version (or cluster identity) subtitle folds
into the name's hover title, matching how the discovery rows already
fold version in. The operational problem line and last-error box stay
as a subtle full-width sub-row, so only rows with an actual issue get a
second line. The row-level install-agent shortcut becomes an icon
button so the Actions cell stays single-line.
This also aligns the configured and cluster-member rows with the
6-column grid the discovery rows already assumed, removing a latent
column-count mismatch. Rows now render at ~29-39px.
Commit 05abf0721 replaced the bare 'k8s'/'kubernetes' -> undefined guard in
canonicalizeAlertTargetType with a real alias table, making metadata.resourceType
a trusted, canonicalized resolution layer that maps 'k8s' to the cluster-level
target. That commit updated the dedicated alertTargetTypes unit test but missed
this older InvestigateAlertButton integration test, which still asserted the
pre-alias 'fall back to agent' behavior. Update the stale expectation to the
current contract (k8s -> k8s-cluster); the inference is correct, the test lagged.
The bundled frontend-modern/public/docs/CONFIGURATION.md lagged the
canonical docs/CONFIGURATION.md after f8ca38587 added the Split-Port
Agent Ingest (Network Isolation) section and the PULSE_AGENT_INGEST_PORT
row. docsLinks.test.ts ('keeps shipped docs content synced with repo
docs') asserts the two are byte-identical; re-copy source -> public to
restore parity.
On a Proxmox node, physical disks collected by the host agent were keyed by
the NVMe controller (e.g. "nvme0 [nvme]") instead of the namespace, reported
sizeBytes 0 (or a stale filesystem-usage value), and flickered as the agent
reading intermittently replaced the authoritative Proxmox disks/list reading.
Root causes:
- smartctl --scan-open reports NVMe disks by their controller char device
(/dev/nvme0), and that scan label became the reported devPath.
- DiskSMART carried no capacity, so the server backfilled size by matching the
SMART device against host filesystem-usage entries, which never match a whole
partitioned/LVM/ZFS disk, leaving size 0.
- The unified-resource merge let the agent's controller label overwrite the
canonical Proxmox /dev/... devPath.
Fixes:
- The agent now reports the canonical block device (an NVMe controller resolves
to its namespace) and the authoritative capacity from /sys/block, with the
smartctl user_capacity / nvme_total_capacity as a cross-platform fallback.
Disks behind multiplexing controllers (megaraid, cciss, areca) keep their
disambiguating label and smartctl-reported size.
- SizeBytes flows through the agent report, host model, and adapter; the
filesystem-usage match is demoted to a legacy fallback.
- The merge keeps a canonical /dev/<device> devPath and never downgrades it to
a scan label, so an un-updated agent can no longer corrupt Proxmox data.
Refs #1483.
Commit 58e0b6da9 ("fix(workloads): show RAM usage in guest drawer Memory card",
2026-05-29) changed getGuestDrawerMemoryRows to always surface primary RAM
usage (Usage / Total / Free) so the Memory card matches the node drawer, with
balloon/swap as optional rows. That commit left a 2026-03-18 test asserting the
old "hide the card when no balloon and no swap" behavior, which has been red
since. Update the test to assert the current intent: the card stays visible and
shows RAM usage even without balloon/swap, while balloon/swap rows remain
conditional. Code unchanged; greens the GuestDrawer suite (59/59).
vSphere VMs render through the shared WorkloadsFilter engine, which already
supported host/namespace scope facets. Add a parallel "Cluster" facet so
operators can scope the vSphere VM list by vCenter cluster, mirroring the
Kubernetes namespace facet end to end but gated to vm view and matching
guest.clusterName. URL-backed via a new `cluster` query param so it is
shareable and captured by saved views, like every other scope filter.
The facet threads through the engine: the `cluster` query param
(resourceLinks), route state + URL sync (useWorkloadRouteState /
useWorkloadUrlSync / workloadUrlSyncModel), distinct-cluster options
(workloadRouteModel), the filter config (workloadFilterConfigModel /
useWorkloadFilterOptions), the filter predicate (workloadSelectors), the
WorkloadsFilter prop + chip, and the vSphere page wiring. The facet only
appears when more than one cluster is present.
Effective-view-mode gating: vSphere forces vm view (forcedViewMode) while the
raw route-state viewMode signal stays 'all'. The cluster facet keys its
validation, clear-on-wrong-view, and URL serialization off the effective view
mode (forced ?? raw), not the raw signal, so forward selection both filters AND
persists to the URL on the forced-vm surface. A unit test pins this:
resolveWorkloadsManagedWorkloadsNavigateTarget serializes cluster when
effectiveViewMode is vm (raw 'all') and not otherwise.
Every change is additive and backward-compatible; existing host/namespace/
context/runtime behavior is untouched.
Verified: type-check, eslint/theme/canonical-platform lint, and the full
Workloads + vmware + proxmox suites (604 pass; the 1 failure is the pre-existing
unrelated GuestDrawer Memory-card test). Live in the browser the Cluster facet
renders in the vSphere filter menu and a deep-link (?cluster=...) filters the VM
list and hydrates the chip. (A post-edit dev-server module-staleness blocked a
final fresh browser pass of forward selection; that path is covered by the new
navigate-target unit test and by type-check enforcing the effectiveViewMode
wiring.)
The Workloads, Services, and Configuration tabs all run on the shared Kubernetes
toolbar driving stacked tables. Generalise the namespace facet across all three
instead of leaving it on Workloads only.
- Extract `createKubernetesNamespaceScope(sections)`: URL-backed namespace facet
(shareable + saved-view-capturable), distinct options derived from the
resource set, pre-filtered sections, and a FilterDef that only appears when
more than one namespace is present. Refactor Workloads onto it (removing the
inline duplicate) and add it to Services and Configuration.
- Each tab folds the namespace into its visible count, hasActiveFilters, and a
combined reset, and passes a per-tab savedViewsKey (kubernetes-workloads /
-services / -config).
Verified: type-check, eslint/theme/canonical-platform lint, FilterBar +
platformPage + kubernetes suites (100 tests), and live in the browser on all
three tabs — Workloads (19 -> 3 on apps), Services (5 -> 3 on apps), and
Configuration (16 -> 1 on apps), each writing `?namespace=` to the URL.
Builds on the shared toolbar's new scope-filter support. The Kubernetes
Workloads tab drives four stacked tables (deployments, pods, workload
controllers, autoscaling) from one shared toolbar, so the namespace facet lives
on that toolbar and scopes all four at once rather than any single table.
- Namespace is URL-backed (useSearchParams) so it is shareable and captured by
saved views, matching the backups-page standard. Distinct namespaces are
derived from the current workload resources; the facet only appears when more
than one namespace is present.
- Each workload section is pre-filtered by namespace before rendering, and the
section visibility gates, the "x of y" counter, hasActiveFilters, and reset
all account for the namespace scope.
Note on placement: a per-table namespace filter on KubernetesPodsTable would be
dead UI, because the page always renders that table with showToolbar=false and
an external (shared) toolbar. The shared Workloads toolbar is the surface the
operator actually sees, so the facet belongs there.
Verified: type-check, eslint/theme/canonical-platform lint, FilterBar +
platformPage + kubernetes suites (100 tests), and live in the browser on
/kubernetes/workloads — applying Namespace scoped 19 -> 3 rows across all
workload sections with a removable chip and `?namespace=` in the URL, a
deep-link `?namespace=monitoring` hydrated to "5 of 19 rows" on load, and Clear
reset everything.
The FilterBar migration gave every platform table the shared toolbar, but they
still only filtered by status. Start closing the "filters are weak" gap by
teaching the shared toolbar to carry scope filters, then wiring the first real
dimension into the busiest table.
Shared enablement (sharedPlatformPage.tsx):
- PlatformTableToolbar gains optional `filters?: FilterDef[]` and
`savedViewsKey?: string`. Scope filters are appended after the inline status
facet and render as chips behind "+ Filter"; savedViewsKey threads through to
the FilterBar's saved-views menu. The public API stays backward-compatible,
so the ~48 existing consumers are untouched.
Docker containers (DockerContainersTable.tsx):
- Add a Host scope filter backed by useSearchParams, so it is shareable via the
URL and captured by saved views (matching the backups-page standard). Distinct
hosts are derived from the current resource set; the facet only appears when
more than one host is present. Rows, the row counter, hasActiveFilters, and
reset all account for the host scope.
Guardrail (platformOverviewLayout.guardrails.test.ts):
- Tighten the bespoke-search-signal check from `const [search` to
`const [search, ` so URL-backed scope state (`const [searchParams, ...] =
useSearchParams`) is not a false positive. Still catches a rogue
`const [search, setSearch] = createSignal`. This keeps the door open for the
same pattern on more tables.
Verified: type-check, eslint/theme/canonical-platform lint, FilterBar +
platformPage + docker suites (94 tests), and live in the browser — applying
Host scoped 8 -> 5 rows with a removable chip and `?host=` in the URL, a
deep-link `?host=ops-services-01` hydrated to "3 of 8 containers" on load, and
Clear reset everything.
The platform overview layout guardrail asserts that Proxmox bespoke detail
tables render through the shared platform table primitives (TableCard,
PLATFORM_TABLE_* classes, getPlatformTableHeadClassForKind/...ForKind). It
checked ProxmoxBackupsTable.tsx, but that file was decomposed into per-view
table components (Coverage/Recoverable/Pbs/Snapshots/Archives/Tasks) and is
now a pure orchestrator that composes them behind the shared FilterBar, so it
no longer renders a table itself.
Repoint proxmoxBespokeTableSources at the six extracted view components, which
each carry the shared primitives. Keeps the guardrail's intent intact while
matching the decomposed structure.
PlatformTableToolbar is the search + status toolbar shared by ~48 platform
tables across Docker, Kubernetes, TrueNAS, vSphere, Proxmox detail, and
Standalone. It rendered a bespoke SearchInput + status FilterButtonGroup,
the pre-FilterBar pattern the backups page just graduated from.
Migrate the toolbar internals onto the shared FilterBar so every platform
table inherits the same combinable-filter UX (chip rail, saved-view
scaffolding, mobile collapse) in one place instead of per-platform reworks.
The public prop surface is unchanged, so all consumers stay as-is:
- search passes straight through to FilterBar's search slot
- the single status facet maps to an inline status FilterDef (segmented
control), preserving custom per-table status option sets
- the visible/total row counter renders via viewOptionsTrailing
- hasActiveFilters/onResetFilters map to FilterBar's showClearAll/onClearAll
Add an optional `tips` field to FilterBarSearch (passed through to the
SearchInput) so AgentsMachinesTable's search tips survive the migration; it
was the only consumer relying on searchTips/searchHistory.
The reset affordance now uses the canonical FilterBar clear button
(aria-label "Clear all"); update the two Docker test suites that asserted
the old "Reset filters" name to match.
Verified: type-check, eslint/theme/canonical-platform lint, FilterBar +
Docker + Kubernetes + Standalone + AgentsMachines suites, and live in the
browser on TrueNAS protection (custom status options) and Docker containers
(search, status filter, counter, and clear-all round trip).