Commit Graph

6774 Commits

Author SHA1 Message Date
rcourtman dfb9bdcd18 Make MSP workspace setup flow first-class 2026-06-01 23:49:30 +01:00
rcourtman d92820cf53 Align MSP signup route tests 2026-06-01 22:56:02 +01:00
rcourtman 00f6ee9daa Make MSP workspace onboarding actionable 2026-06-01 22:41:10 +01:00
rcourtman 59266ea4f0 Stabilize TrueNAS recovery poller tests 2026-06-01 22:21:21 +01:00
rcourtman d2639fadad Refresh frontend bundle size baseline 2026-06-01 21:50:43 +01:00
rcourtman e550e40f51 Align platform contract test expectations 2026-06-01 21:43:26 +01:00
rcourtman 9176a2fa76 Route managed hosting links to support 2026-06-01 21:10:37 +01:00
rcourtman 75ec4ac8eb Align MSP workspace limit governance 2026-06-01 19:00:56 +01:00
rcourtman 184f8244da Record pre-GA public checkout posture 2026-06-01 18:47:15 +01:00
rcourtman 5afe1845cf Report target RC telemetry signal coverage 2026-06-01 17:59:28 +01:00
rcourtman c61bd1c554 Show telemetry deep-signal sources by version 2026-06-01 17:50:22 +01:00
rcourtman 22115e4061 Record hosted runtime hibernation policy 2026-06-01 17:16:13 +01:00
rcourtman 9a61fe761e Record MSP provider-hosted launch stance 2026-06-01 17:08:07 +01:00
rcourtman 914465e67f Tighten MSP workspace tier limits 2026-06-01 16:55:23 +01:00
rcourtman e450c6a4da Lock MSP Starter self-serve checkout 2026-06-01 16:19:24 +01:00
rcourtman e2216b6afb Reduce Proxmox backup coverage table density 2026-06-01 14:40:42 +01:00
rcourtman d8f154addd Fix alert escalation notification scheduling
Refs #1444
2026-06-01 14:16:57 +01:00
rcourtman a464d3cf6b Color-code Proxmox backup ages 2026-06-01 14:13:06 +01:00
rcourtman 3e0c7be81c Clarify Proxmox backup table columns 2026-06-01 11:59:48 +01:00
rcourtman b4e1957c47 Clarify Proxmox backup source labels 2026-06-01 11:35:57 +01:00
rcourtman 74def6b9ad Canonicalize workload type badges 2026-06-01 11:12:28 +01:00
rcourtman 6140837dcf Restore host backup coverage targets
Refs #454
2026-06-01 10:36:04 +01:00
rcourtman c51f68817d Keep host backups out of guest coverage 2026-06-01 10:08:30 +01:00
rcourtman 9eac936c8e Fix add-filter placeholder state 2026-06-01 09:51:46 +01:00
rcourtman 4cf7371976 Consolidate settings paywalls into a shared FeatureGateSection
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.
2026-06-01 09:38:22 +01:00
rcourtman 4845e96039 Add Proxmox backup date feed 2026-06-01 09:30:04 +01:00
rcourtman 6abcac3fde Keep live resource metrics fresh 2026-06-01 08:59:56 +01:00
rcourtman 775100f7e1 Record platform workflow decisions
Capture the governed decisions for Docker Overview host-plus-workloads, evidence-gated platform workflow tabs, and mock-mode restart recovery proof.
2026-06-01 08:45:46 +01:00
rcourtman 64cbdd6e42 Gate platform workflow tabs by inventory
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.
2026-06-01 08:41:49 +01:00
rcourtman a13218a39a Recover managed mock-mode restart handoffs
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.
2026-06-01 08:33:18 +01:00
rcourtman b8e47ab7fa Remove MSP signup trial checkout
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.
2026-06-01 00:24:02 +01:00
rcourtman 253bc56c19 Align storage recovery registry with Proxmox backup surface
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.
2026-06-01 00:22:27 +01:00
rcourtman 0b4a31ba30 docs(multi-tenant): document the central-server MSP onboarding workflow
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.
2026-05-31 23:03:31 +01:00
rcourtman 452c55f9ec Hide empty Docker inventory tabs
Only show Docker workflow tabs when canonical resource evidence exists for that section. Keep direct empty-tab routes falling back to Overview.
2026-05-31 13:50:09 +01:00
rcourtman cfa9283c98 Collapse Proxmox backups surface to a guest-centric coverage view
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.
2026-05-31 09:19:08 +01:00
rcourtman af358ef365 hot-dev: isolate mock data dir so mock/real switching is clean
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.
2026-05-30 19:28:25 +01:00
rcourtman 4f52ef88da Don't surface agent-fleet governance on pull-based API sources
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.
2026-05-30 18:54:24 +01:00
rcourtman ab9a439f2c Fix agent Manage modal: drop unusable Pause, declutter layout
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.
2026-05-30 11:20:05 +01:00
rcourtman dbf7572f67 Flatten connected-systems settings table to single-line rows
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.
2026-05-29 23:17:19 +01:00
rcourtman 1e1aebd174 Align Investigate alert metadata test with k8s target contract
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.
2026-05-29 20:34:32 +01:00
rcourtman 5152d2eb45 docs: sync shipped CONFIGURATION copy with split-port agent ingest
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.
2026-05-29 20:32:18 +01:00
rcourtman bd20069c60 fix(disks): report authoritative disk size and namespace devpath from the host agent
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.
2026-05-29 19:55:53 +01:00
rcourtman 42105ec6e5 test(workloads): realign stale Memory-card test with shipped behavior
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).
2026-05-29 19:31:53 +01:00
rcourtman 6147562ce1 feat(vmware): add a cluster scope filter to the vSphere workloads view
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.)
2026-05-29 18:11:18 +01:00
rcourtman e6a93632a6 feat(kubernetes): extend the namespace scope filter to Services and Configuration
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.
2026-05-29 16:59:17 +01:00
rcourtman 26efed554e feat(kubernetes): add a namespace scope filter to the Workloads tab
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.
2026-05-29 16:49:10 +01:00
rcourtman 4c0eb398e9 feat(docker): add a URL-backed Host scope filter to the containers table
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.
2026-05-29 16:29:39 +01:00
rcourtman d25007d390 test(proxmox): follow backups table decomposition in the platform guardrail
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.
2026-05-29 15:49:43 +01:00
rcourtman 911fce9614 frontend: migrate shared PlatformTableToolbar onto the canonical FilterBar
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).
2026-05-29 15:43:17 +01:00
rcourtman bcbec3acdb Fix release smoke for installer endpoints (#1470) 2026-05-29 15:10:09 +01:00