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).
The Workload coverage table always showed PBS / Archive / Snapshot / Latest
task columns even when no workload anywhere had that data, so a PBS-only fleet
stared at empty Archive and Snapshot columns. Those four columns now drop out
when no workload has the corresponding data (computed over the full coverage
set, not the filtered view, so columns don't flicker as filters change), and
the table-fixed colgroup re-normalizes the remaining columns. This matches how
the source-detail tables (job history size/error, archives PBS columns,
snapshot size/RAM) already hide their conditional columns.
Chosen over a manual column-picker (which would mean a six-table ColumnDef /
useColumnVisibility refactor) because it solves the empty-column problem
directly with no menu to manage. Adds ProxmoxCoverageTable.test.tsx asserting
columns drop when their show flag is false; verified live that all columns
still render when every source has data.
Root fix for the recurring class behind issue #1470. The served /install.sh and
/install.ps1 endpoints existed to hand out the unified AGENT installer, but their
GitHub fallback fetched the top-level install.sh release asset, which since
49412357a is the SERVER installer. Prior commits made that fallback unreachable
in normal deployments (deploy the sidecars; serve the local script even when
unsigned), but the endpoint was still structurally capable of serving the wrong
script in the no-local-bundle case.
The agent installer is a per-build artifact bundled into every release tarball and
Docker image, not a release asset, so the endpoint has no business proxying a
release asset at all. Remove proxyInstallScriptFromGitHub and its
installScriptReleaseAssetURL wrapper. handleDownloadInstallScriptCommon now serves
the locally bundled agent installer (signed when sidecars are present, unsigned
otherwise) or fails closed with 503 when no bundled script exists. Serving the
SERVER installer at this endpoint is now structurally impossible, not merely
unreachable.
The shared version-pinning (releaseAssetTag/releaseAssetURL) and installScriptClient
remain for the agent-BINARY download proxy, which legitimately fetches published
release assets; its version-pinning stays covered by the agentBinaryReleaseAssetURL
contract tests.
Replace the obsolete install-script proxy tests with fail-closed assertions
(including a guard that the endpoint makes no outbound call), drop the four
installScriptReleaseAssetURL contract tests, and revise the four subsystem
contracts that pinned the install-script fallback transport (api-contracts items
8 and 27, agent-lifecycle item 14, storage-recovery item 14, plus the
deployment-installability note) to state that install scripts are served local or
fail closed with no GitHub fallback.
Moves the Node and Type scope filters to the URL (?node, ?type via
useSearchParams) so filtered views are shareable, bookmarkable, and survive
reload, and enables FilterBar saved views (savedViewsKey "proxmox-backups").
useSavedViews persists the URL query string, so URL-backed filters are the
prerequisite. Search, the per-view status facet, and the chart day stay
ephemeral signals (kept out of the URL to avoid per-keystroke churn and the
per-tab facet-encoding mess).
Verified live: setting Node=pve1 writes ?node=pve1 and filters 20 -> 8;
reload restores the filter from the URL (chip shown, 8 rows); the Saved-views
menu and a Reset (clear-all) render. The test now renders inside a Router
since the component reads search params.
The "Install on Linux/Windows" wizard does `curl -fsSL <server>/install.sh |
bash -s -- --url ...` and never verifies the response signature headers (curl|bash
discards them). But for published releases handleDownloadInstallScriptCommon
proxied the top-level GitHub install.sh asset whenever the local agent installer
lacked its .sig/.sshsig sidecars, and since 49412357a that asset is the SERVER
installer, which rejects --url. Every install missing the sidecars served the
wrong script. The companion deploy_agent_scripts fix deploys the sidecars for new
installs, but existing boxes stay broken until they redeploy.
Serve the locally bundled agent installer when its signatures are absent instead
of proxying. An unsigned-but-correct local script beats a signed-but-wrong proxied
one when nothing verifies the headers, and this retroactively fixes already-deployed
boxes the moment they get the new binary. The proxy now runs only when no local
installer is bundled at all, so the endpoint can no longer hand the agent wizard a
server installer in any reachable deployment state. New installs still ship the
sidecars and are served signed.
Revise the install-script signature/fallback contract this changes, across the
three subsystems that pin it (api-contracts item 8, agent-lifecycle item 14,
storage-recovery item 14) plus the deployment-installability note, to state that
the served endpoint serves the agent installer with correctness outranking
signature presence. Add a handler guard asserting a published-release server with
a present-but-unsigned local installer serves it locally and does not proxy.
Adds a VM / Container / Host filter to the backups FilterBar, applied across
every tab including PBS (via backupType). The backups data mixes type
vocabularies (recovery model and PBS use vm/ct/host; raw PVE tasks, archives,
and snapshots use qemu/lxc), so a small canonicalGuestType normalizer maps
each shape to vm/ct/host before comparing.
Combines with the Node and status filters. Verified live: coverage 19 -> 4
under "VMs", and the Job-history tab correctly shows QEMU tasks under the same
"VMs" option (qemu -> vm normalization). Scope filter, in the "+ Filter" menu.
Adds a Node filter to the backups FilterBar, the biggest gap on a multi-node
fleet. A nodeFilter signal plus a nodeOptions memo (the distinct nodes across
the workload set) feed a scope FilterDef shown in the "+ Filter" menu on every
node-bearing view; it is applied as a predicate in the coverage, restore-points,
snapshots, archives, and job-history memos. PBS artifacts carry no node, so the
filter is omitted there.
Combines with the existing per-view status facet (e.g. uncovered workloads on a
specific node, failed tasks on a node). Verified live: selecting a node filters
the coverage table 20 -> 8 rows, and the control renders identically to the
overview's shared AddFilterMenu.
The running server serves the unified AGENT installer at its /install.sh
endpoint, which the "Install on Linux" wizard fetches with --url/--token-file.
For published releases handleDownloadInstallScriptCommon only serves the local
/opt/pulse/scripts/install.sh when its .sig and .sshsig sidecars sit next to it;
otherwise it proxies the top-level GitHub install.sh asset. Since 49412357a that
asset is the SERVER installer (LXC/systemd quickstart, --version), so the proxy
now hands the agent wizard a server installer that rejects --url with
"Unknown option".
deploy_agent_scripts copied only the .sh/.ps1 files, never the sidecars, so every
published-release LXC/systemd install always hit the proxy and served the wrong
script. The Docker image already deploys both sidecars (Dockerfile); LXC/systemd
did not. This was latent across rc.1-rc.5 (the proxied asset was the agent
installer then) and surfaced in rc.6 once the asset flipped to the server
installer.
Deploy the .sig/.sshsig sidecars alongside each script so the endpoint serves
the local agent installer and never falls back to the server-installer asset.
Add a behavioral guard that runs deploy_agent_scripts and asserts the sidecars
land next to the served script, and document the served-endpoint sidecar
requirement in the deployment-installability contract.
Replaces the backups page's bespoke search box + per-tab single-select
FilterButtonGroup chip row with the shared FilterBar (the same component the
overview, workloads, storage, and alerts pages use). Each tab's status facet
is re-expressed as an inline FilterDef via buildBackupsFilters(); the
chart-driven date selection is preserved as a clearable chip in searchTrailing,
and the results count moves to viewOptionsTrailing.
Behavior-preserving (same filters, same signals, same memos) but on the
canonical filter UX: removable chips plus a "+ Filter" menu and saved-views
slot, ready for the Node and Type scope filters that follow. The per-tab
search placeholder collapses to one generic string since FilterBar's
placeholder is static. Verified live: search + posture facet render and
filter (All 20 -> Attention 19 -> restored 20), zero console errors.
The coverage and restore-points tables rendered workload.label, which appends
the identity in brackets after the name ("reporting-api-01 (CT 106)"). That
read poorly. Both now show the clean workload.name in the Workload column and
the vmid in its own VMID column (muted, tabular), matching the overview
workloads table which already has a dedicated VMID column. Falls back to the
label when a workload has no distinct name (e.g. host backups), so the column
is never empty and never bracketed.
The other backups tables (PBS, snapshots, archives, job history) have no
workload name, so they already showed a plain "CT 106" with no brackets and
are unchanged. Tests updated to assert the workload name on its own.
Prefer QEMU guest-agent MemAvailable when Proxmox reports saturated VM memory without guest free fields.
Add regression coverage for the issue #1319 Windows fsinfo volume payload so usable C/E/F volumes remain counted while System Reserved partitions are skipped.
Refs #1319
Completes the backups-table flattening. Restore points stacked the workload
name over a redundant "type vmid" subtitle (already in the label); PBS
artifacts stacked the same on Workload plus repository over instance. Both now
render single-line rows (~29px, matching the overview): the recoverable
Workload cell drops the redundant subtitle, and the PBS Repository cell drops
the instance subtitle with the instance promoted to its own column.
Snapshots, Archives, and Job history already rendered single-line main rows,
so the whole backups page is now flat-column and scannable. Gates green;
verified live (zero multi-line rows, 29px rows on both tables).
The Workload coverage table stacked two facts per cell (workload name over
type/node, restore age over source, per-source age over count/location, task
status over time), giving 41px rows that were harder to scan than the
single-line overview tables. Flattens it to one datum per single-line column:
Workload, Node (promoted to its own column), Posture, Latest restore, PBS,
Archive, Snapshot, Latest task. Rows drop to ~33px with no stacked text,
matching the Proxmox overview's density.
The per-source count/location, the latest-restore source, and the exact task
time move to the existing row expansion (which already lists full per-artifact
evidence), since promoting all of them to columns would mean 15+ columns and
horizontal scroll. Removes the now-unused RecoverySourceSummary from the
shared module. Test updated to assert the PBS repository label on the PBS
artifacts tab rather than inline on the coverage row.
Final decomposition step. Moves the "Workload coverage" table (the last and
largest inline view) out of ProxmoxBackupsTable into a presentational
ProxmoxCoverageTable component: the per-workload posture row across
PBS/archive/snapshot/task plus the per-row expansion into restore evidence.
The parent keeps the filtered+sorted memo, shared filters, and the expansion
set; the pure coveragePostureVariant helper moves into the component.
Bakes in table-fixed + a colgroup so the outer columns no longer balloon; the
inner evidence table stays content-sized. Behavior-preserving; verified live:
posture badges and source-summary columns render, expanding a row shows its
restore-evidence table (or the empty-evidence fallback), zero console errors.
ProxmoxBackupsTable is now fully decomposed: 3295 -> 1566 lines, a pure
orchestrator over the model, shared primitives, and six view components.
Seventh decomposition step and the first expandable view. Moves the "Source
details > Snapshots" table out of ProxmoxBackupsTable into a presentational
ProxmoxSnapshotsTable component: the per-guest rows plus the per-guest
expansion into an inner snapshot list. The parent keeps the filtered+sorted
memo, shared filters, and the expansion set, passing it as expandedKeys +
onToggleExpand. The SnapshotGuestRow row shape moves to the model so both the
parent memo and the component share it.
Bakes in table-fixed + a weighted colgroup on the outer table (handling the
conditional Total size / RAM columns); the inner per-snapshot table stays
content-sized. Behavior-preserving; verified live: rows render, clicking a
guest expands its snapshot list and rotates the chevron, zero console errors.
The orchestrator drops from 2167 to 1911 lines.
Sixth decomposition step and the last flat backups view. Moves the "Source
details > Backup files" (archives) table out of ProxmoxBackupsTable into a
presentational ProxmoxArchivesTable component taking the filtered archives,
sort accessors/handler, the conditional PBS-columns flag, the size scale, and
the now timestamp (for the age swatch) as props. The parent keeps the
filtered+sorted memo and shared filters.
Bakes in table-fixed + a weighted colgroup (handling the conditional
Protection/Verified columns) so columns no longer balloon on wide viewports.
Behavior-preserving; verified live at 1800px with the PBS columns shown.
The orchestrator drops from 2366 to 2167 lines.
Fifth decomposition step. Moves the "Source details > PBS artifacts" view out
of ProxmoxBackupsTable into a presentational ProxmoxPbsTable component that
owns its error / loading / empty / table states and takes the filtered
backups, sort accessors/handler, size scale, and resource-derived
error/loading/refresh as props. The parent keeps the filtered+sorted memo and
the shared search/status filters.
Bakes in the table-fixed + colgroup layout fix so the Files column no longer
balloons on wide viewports. Behavior-preserving; verified live at 1800px
(balanced columns, sorting and verification/protection badges intact, zero
console errors). The orchestrator drops from 2571 to 2366 lines.
The Job-history and Restore-points tables used w-full with table-layout auto,
so once the container was wider than the columns' content the slack all went
into the last column. The Error column ballooned to ~440px of blank when no
rows had errors, and Details did the same on wide monitors, squashing the rest
of the columns to the left.
Switches both tables to table-fixed with a colgroup that shares the width
across columns (weighted for the conditional Size/Error columns on Job
history), matching the canonical platform-table approach. Verified at 1156px
and 1800px: columns now span the row evenly with no blank gulf.
PULSE_AGENT_INGEST_PORT existed in code but was absent from operator-facing
docs, so the network-isolation feature (dedicated agent-ingest port serving
only /api/agents/*, firewalled from the web UI/management API) was
undiscoverable. Adds it to CONFIGURATION.md (env-var row + Split-Port Agent
Ingest section with the security model and agent wiring via
PULSE_AGENT_CONNECT_URL) and an API Security cross-reference in
AGENT_SECURITY.md. Also aligns WEBHOOKS.md multi-tenant licensing wording with
MULTI_TENANT.md / API.md (Enterprise license + multi_tenant capability).
Fourth decomposition step. Moves the "Job history" table render out of
ProxmoxBackupsTable into a presentational ProxmoxTasksTable component that
takes the filtered tasks, sort accessors/handler, the optional size/error
column flags, and the median duration baseline as props. The parent keeps the
filtered+sorted memo, the shared search/status filters, and the activity
chart + outcomes strip.
Behavior-preserving; verified live (renders identically, status dots and
median-anchored duration bars intact, column sorting works through the
threaded accessors, zero console errors). The orchestrator drops from 2753 to
2571 lines.
Third decomposition step, and the first per-view carve. Moves the "Restore
points" table render out of ProxmoxBackupsTable into a presentational
ProxmoxRecoverableTable component that takes the filtered artifacts, sort
accessors/handler, and size scale as props. The parent keeps ownership of the
shared search/day/source filter state and the filtered+sorted memo.
Also moves the pure artifactStateLabel helper into the shared module (it is
used by the parent's sort comparator and other views too).
Behavior-preserving; verified live (renders identically, column sorting works
through the threaded accessors, zero console errors). The orchestrator drops
from 2909 to 2753 lines.
Second decomposition step. Moves the JSX presentational pieces that the
backups sub-views share into proxmoxBackupsTableShared.tsx: the six filter
option catalogs (statusDot-backed) and the small reusable components
RowMetricBar, RecoverySourceSummary, ArtifactStateBadge, ArtifactSourceBadge,
and SortableHead. These are now importable by the future per-view components.
Also relocates the cmpString rationale comment to the model (it was stranded
in the component when the comparator moved in the previous step) and drops a
dead comment that documented no declaration.
Behavior-preserving; verified live across the coverage, restore-points,
source-detail, and job-history tables. The component drops from 3134 to 2909
lines.
First step of decomposing the 3295-line ProxmoxBackupsTable monolith. Moves
the JSX-free pieces into a new proxmoxBackupsTableModel.ts, matching the
existing sibling-model pattern (proxmoxBackupSummaryPresentation,
proxmoxBackupRecoveryModel): tab/source-detail and filter-value types, the
six per-tab sort-key types and their default-direction tables, the sort
comparators (cmpString/cmpNumber/cmpBool), and the label/duration/status
helpers (classifyTaskStatus, guestLabel, pbsWorkloadLabel, pbsRepositoryLabel,
formatDuration, formatDurationFromSeconds).
Behavior-preserving; adds proxmoxBackupsTableModel.test.ts covering the
extracted logic. The component drops from 3295 to 3134 lines. The reactive
memos, filter toolbars, and rendering stay in the component for now; sub-view
extraction follows in separate commits.
After the nodes table started fitting its container below the wide layout,
the count columns (weight 4) were narrow enough that the "VMs" header
truncated to "V.." at common laptop widths while "CTs" still fit, looking
asymmetric. Bumping both to weight 5 lets the headers render in full
(VMs/CTs ~62px at a 1156px container) at a negligible cost to the metric
bars (CPU/Memory/Disk ~162px, still above their 140px target).
The Proxmox nodes table reserved a fixed min-w-[1240px] across the entire
compact layout band (900-1440px viewport), which covers most laptops. On any
container narrower than 1240px the rightmost column (Cluster) sat behind a
horizontal scroll that is easy to miss. Because the table is table-fixed with
percentage column widths, only the wide layout (>= 1440px) now reserves the
fixed floor; compact and below use min-w-full so the table fits its container
and the metric bars scale down gracefully. This also matches the workloads
table rendered directly below, which already fits its container.
The guest drawer Overview card titled Memory only rendered supplementary
balloon/swap lines, never the primary RAM used/total, so an expanded guest
showed a Memory card containing just a swap figure. It now leads with
Usage/Total/Free (mirroring the node drawer Memory card) and keeps balloon
and swap as trailing rows.
Replaces the string-only getGuestDrawerMemoryExtraLines with a structured
getGuestDrawerMemoryRows and removes the now-dead memoryExtraLines prop from
the guest drawer chain.
Rename the Pro-gated "Alert-Triggered Analysis" toggle to "Container
Update Risk" with container-update-scoped copy. The enterprise
AlertTriggeredAnalyzer only assesses docker-container-update alerts and
returns nil for all other types, so the old general name was misleading
and collided with the genuinely general "Alert-Triggered Patrols"
control. Reorder the panel so the explanatory box leads, followed by
Alert-Triggered Patrols plus its severity selector, Anomaly-Triggered
Patrols, Container Update Risk, then Autonomous critical remediation.
The rename is presentation-only: persisted settings keys
(alert_triggered_analysis, patrol_alert_triggers_enabled,
patrol_alert_trigger_min_severity) are unchanged.
Adds a "Multi-tenant / MSP and PSA integration" section to WEBHOOKS.md
covering per-org isolation, org resolution order, and the two PSA
integration models (push via per-org Generic webhook, pull via
org-scoped read API). Documents existing capability; no code change.
Operators can set PULSE_AGENT_INGEST_PORT to serve agent report and
management traffic (/api/agents/*) on a second listener, so that surface
can be placed on its own network or firewall boundary without exposing
the web UI or the rest of the REST API on that port. The dedicated port
serves only the /api/agents/* prefix and 404s everything else.
The option is additive and fail-closed: it is disabled at 0, the main
listener keeps serving agent ingest so existing single-port deployments
and agents are unaffected, and validation rejects out-of-range ports or
collisions with the frontend or HTTP redirect ports.
Previously an alert that triggered Patrol ran a broad health check that
explicitly ignored the threshold breach. Now an alert carries its real
payload (metric type, value, threshold, identifier, level, message) into
the patrol scope, and the alert_fired run is framed around root-causing
that specific breach instead of a general assessment.
Three coordinated changes:
- Carry the alert payload into PatrolScope.AlertContext through the alert
bridge (PatrolTriggerEvent), so the patrol prompt sees the breach
specifics rather than just an alert-type string.
- Frame alert_fired patrol runs around the breach: replace the
"ignore threshold breaches" instruction with a root-cause directive
targeting the alert's metric and threshold.
- Add per-rule control via AIConfig.AlertTriggersInvestigation: a master
enable, a minimum-severity floor (patrol_alert_trigger_min_severity,
default critical-only), and an optional alert-type allowlist
(patrol_alert_trigger_types). The router's bridge callback consults the
policy and drops non-qualifying alert_fired events before queuing a
scoped patrol. A config-panel selector persists the severity floor.
Adds config, handler, and frontend proof tests, and updates the affected
subsystem contracts.
Adds public self-serve signup for the hosted MSP offering alongside the
existing individual Cloud signup page. New handlers serve /cloud/msp/signup,
/cloud/msp/signup/complete, and /api/public/msp/signup, gated behind the same
PublicCloudSignupEnabled flag. Per-tier Stripe prices
(CP_MSP_STARTER_PRICE_ID, CP_MSP_GROWTH_PRICE_ID, CP_MSP_SCALE_PRICE_ID) are
validated against the canonical msp_starter/growth/scale plan versions; the
page renders an explicit "not open yet" notice when no MSP price is
configured. Checkout sessions carry account_kind=msp metadata so the
provisioner seeds an isolated operator workspace. The cloud and MSP JSON
signup endpoints now share one checkout skeleton parameterized per path.
The alert thresholds platform toggle and its sections still used the
older "Systems" / "System Disks" vocabulary while the rest of the
product (top nav, the standalone page, the AgentMachine data model)
settled on "Machines". Align the toggle, section titles, override
summary, and empty states so the thresholds page matches the canonical
nav term. Internal tab keys, routes, and the resourceType payload are
unchanged.