Make Proxmox onboarding API-first

This commit is contained in:
rcourtman 2026-05-05 13:25:17 +01:00
parent cf103ca9fe
commit c61ea4947a
16 changed files with 214 additions and 197 deletions

View file

@ -60,6 +60,8 @@ If Proxmox API data is enough for your use case, prefer API-only monitoring and
do not install a host agent just because the installer exists. Install agents
where you need data that Proxmox cannot provide through its API, or where the
data lives inside a guest/container rather than at the Proxmox node layer.
The Settings Proxmox setup flow uses this API inventory path as the default;
the host telemetry agent path is for the full-telemetry cases above.
Running `pulse-agent` as a custom non-root systemd user is possible by editing
the service unit, but it is not a supported full-telemetry mode today. Expect

View file

@ -1,17 +1,19 @@
# Pulse Unified Agent
The unified agent (`pulse-agent`) combines host, Docker, and Kubernetes monitoring into a single binary. It replaces older split-agent installs with one deployment and one service for simpler operations.
Install it on each host you want Pulse to monitor. This is the primary monitoring path for infrastructure onboarding.
Install it on standalone hosts and on machines where Pulse needs full node-local telemetry.
For API-backed platforms, start with the platform connection first and add the agent only where local telemetry is needed.
For Proxmox, install the agent only where you need telemetry that the Proxmox
API cannot provide, such as inside-guest Docker/Podman visibility, host SMART
and temperature data, local ZFS/Ceph/mdadm detail, or arbitrary mount reads.
Basic Proxmox inventory and utilization can use a read-only or narrowly scoped
Proxmox API token instead. See [Agent Security](AGENT_SECURITY.md) for the
root-service trade-off, restricted-user expectations, and supply-chain
Proxmox API token instead. Settings uses that API inventory path as the
default for new PVE/PBS setup. See [Agent Security](AGENT_SECURITY.md) for
the root-service trade-off, restricted-user expectations, and supply-chain
verification guidance.
> Note: For temperature monitoring, use `pulse-agent --enable-proxmox` (recommended) or SSH-based collection. The legacy sensor proxy has been removed. See `docs/TEMPERATURE_MONITORING.md`.
> Note: For agent-based temperature monitoring, use `pulse-agent --enable-proxmox` or SSH-based collection. The legacy sensor proxy has been removed. See `docs/TEMPERATURE_MONITORING.md`.
## Quick Start

View file

@ -0,0 +1,42 @@
# Agent Lifecycle Proxmox API-First Onboarding
Date: 2026-05-05
## Decision
New Proxmox VE and Proxmox Backup Server setup defaults to the API Inventory
path. The root Pulse Agent install remains supported, but it is presented as
the Host Telemetry Agent path for full node-local telemetry rather than as the
general Proxmox default.
## Evidence
- `frontend-modern/src/utils/nodeModalPresentation.ts` now initializes new
PVE/PBS node setup in `auto` mode, so the setup guide opens on the API
inventory command path.
- `frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx`
labels API Inventory as the recommended least-privilege path and labels the
agent command as optional full host telemetry for SMART, temperatures, local
storage details, and agent-driven operations.
- `frontend-modern/src/utils/infrastructureOnboardingPresentation.ts` now
describes Proxmox-family onboarding as API-first with the agent optional
where node-local telemetry is needed.
- `docs/AGENT_SECURITY.md` and `docs/UNIFIED_AGENT.md` carry the same operator
guidance: start with Proxmox API inventory and install agents only where
the API cannot provide the required data.
## Proof Commands
Run as part of this slice:
- `npm --prefix frontend-modern test -- --run src/components/Settings/__tests__/NodeModalSetupGuideSection.test.tsx src/utils/__tests__/nodeModalPresentation.test.ts src/utils/__tests__/infrastructureOnboardingPresentation.test.ts src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx src/components/Settings/__tests__/settingsArchitecture.test.ts`
- `npm --prefix frontend-modern run type-check`
- Playwright inspection of the Settings Infrastructure Proxmox add flow at
desktop and mobile viewports after the frontend build updated.
## Follow-Up Guard
Future Proxmox setup copy may still recommend a host agent for temperatures,
SMART, Docker/Podman, local storage detail, and command execution, but it must
not make the root agent install the default route for ordinary Proxmox API
inventory.

View file

@ -3360,6 +3360,11 @@
"path": "docs/release-control/v6/internal/records/agent-lifecycle-ai-discovery-adapter-origin-2026-05-01.md",
"kind": "file"
},
{
"repo": "pulse",
"path": "docs/release-control/v6/internal/records/agent-lifecycle-proxmox-api-first-onboarding-2026-05-05.md",
"kind": "file"
},
{
"repo": "pulse",
"path": "docs/release-control/v6/internal/records/agent-lifecycle-root-agent-hardening-2026-05-05.md",
@ -3405,6 +3410,31 @@
"path": "frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Settings/NodeModalAuthenticationSection.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/components/Settings/useNodeModalState.ts",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/utils/infrastructureOnboardingPresentation.ts",
"kind": "file"
},
{
"repo": "pulse",
"path": "frontend-modern/src/utils/nodeModalPresentation.ts",
"kind": "file"
},
{
"repo": "pulse",
"path": "internal/agentupdate/update.go",

View file

@ -142,14 +142,14 @@ only; lifecycle surfaces must not present them as executable, dispatch them
through agent-local command paths, or bypass the API fail-closed execution gate.
The node setup modal boundary must keep guided setup and manual credential
submission separate. For new PVE/PBS setup, Agent Install and Direct Connection
setup-script modes are command-driven auto-registration paths; Token ID/Value
submission separate. For new PVE/PBS setup, API Inventory and Host Telemetry
Agent setup modes are command-driven auto-registration paths; Token ID/Value
fields, Test Connection, and Add Node submission belong only to Manual Token
Setup or existing-node edit flows.
The setup guide must also present the source strategy at action time: Agent
Install is the recommended API + Agent path, Direct Connection is the advanced
API inventory path, and Manual Token Setup is an advanced manual API-token
escape hatch.
The setup guide must also present the source strategy at action time: API
Inventory is the recommended least-privilege API path, Host Telemetry Agent is
the optional full-host-telemetry root-agent path, and Manual Token Setup is an
advanced manual API-token escape hatch.
That shared monitored-system impact preview boundary also owns the disabled
platform-connection lifecycle state. Once a TrueNAS or VMware setup form marks
@ -772,6 +772,11 @@ The supported full-telemetry systemd agent may still run as `root`, but
in the rendered service, and generated systemd units must keep conservative
sandboxing in place unless a future telemetry requirement records a narrower
exception.
New Proxmox VE and Proxmox Backup Server setup must default to the API
Inventory path: the UI may recommend a root Pulse Agent only as the Host
Telemetry Agent path for temperatures, SMART, local storage detail,
agent-driven operations, or other node-local telemetry the Proxmox API cannot
provide.
Generated TrueNAS CORE rc.d service scripts must give `/usr/sbin/daemon -r` a
supervisor pidfile with `-P`, keep the child pid in a separate diagnostic

View file

@ -162,13 +162,13 @@ product API routes free of maintainer commercial analytics.
30. `frontend-modern/src/utils/infrastructureSettingsPresentation.ts` shared with `agent-lifecycle`: the infrastructure settings presentation helper is both an agent lifecycle control surface and an API-backed direct-node/discovery settings boundary.
31. `internal/api/access_control_handlers.go` shared with `organization-settings`: RBAC role and user-assignment handlers are both an organization settings control surface and a canonical API payload contract boundary.
The shared node setup boundary above owns the guided/manual setup split
for PVE/PBS consumers: Agent Install and Direct Connection setup-script
modes are auto-registration paths, while Token ID/Value fields, Test
Connection, and Add Node are manual-token or existing-node edit controls
only.
for PVE/PBS consumers: API Inventory and Host Telemetry Agent setup modes
are auto-registration paths, while Token ID/Value fields, Test Connection,
and Add Node are manual-token or existing-node edit controls only.
That same client contract must expose the setup strategy before a token
path is chosen: Agent Install is API + Agent, Direct Connection is API
inventory, and Manual Token Setup is a manual API-token escape hatch.
path is chosen: API Inventory is the recommended least-privilege API path,
Host Telemetry Agent is the optional full-host-telemetry root-agent path,
and Manual Token Setup is a manual API-token escape hatch.
The inline node credential slot must keep the visible submit sequence as
`Endpoint`, `Authentication`, and `Coverage` before the API-backed setup
controls. That sequence is presentation guidance for the existing setup
@ -2531,10 +2531,10 @@ masked: copy-success messaging may not tell the operator to paste a token
cleanup paths may not survive in one Proxmox branch after the shared UI state
has moved to hint-only handling.
That same shared settings consumer must keep command-driven setup and manual
credential submission distinct. When a new PVE/PBS setup is in Agent Install or
Direct Connection setup-script mode, the settings UI must not render Token
ID/Value fields, Test Connection, or Add Node controls; those controls are only
valid for Manual Token Setup or existing-node edit flows.
credential submission distinct. When a new PVE/PBS setup is in API Inventory or
Host Telemetry Agent mode, the settings UI must not render Token ID/Value
fields, Test Connection, or Add Node controls; those controls are only valid for
Manual Token Setup or existing-node edit flows.
That same shared frontend setup surface must also trim and validate the
canonical `host` input before invoking `/api/setup-script` downloads, and the
shared `frontend-modern/src/api/nodes.ts` helper must reject empty `host` or

View file

@ -110,7 +110,7 @@ export const NodeModalAuthenticationSection: Component<NodeModalAuthenticationSe
<Show when={state.formData().setupMode === 'manual'}>
<div class="rounded-md border border-border bg-surface-alt px-3 py-2 text-xs leading-5 text-muted">
Advanced manual token path: use this only when you already created the API token
yourself. Agent Install and Direct Connection register the source automatically
yourself. API Inventory and Host Telemetry Agent register the source automatically
without copying token credentials into this form.
</div>

View file

@ -19,15 +19,15 @@ const getNodeSetupStrategyPresentation = (
if (setupMode === 'agent') {
return {
label: 'API + Agent',
detail: `Recommended: creates the ${productLabel} API token, installs Pulse Agent, and registers the source automatically. No token fields are needed in this form.`,
label: 'Host telemetry agent',
detail: `Optional full host telemetry: creates the ${productLabel} API token, installs Pulse Agent as the supported root service, and registers the source automatically.`,
};
}
if (setupMode === 'auto') {
return {
label: 'API inventory',
detail: `Advanced: creates the ${productLabel} API token and registers the API connection, but does not install Pulse Agent for node-local telemetry.`,
detail: `Recommended least-privilege path: creates the ${productLabel} API token and registers the API connection without installing a root agent.`,
};
}
@ -38,6 +38,13 @@ const getNodeSetupStrategyPresentation = (
};
};
const setupModeButtonClass = (selected: boolean): string =>
`inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md border border-transparent transition-colors ${
selected
? 'bg-surface text-blue-600 dark:text-blue-300 border-border shadow-sm'
: 'text-muted hover:text-blue-600 dark:hover:text-blue-300 hover:bg-surface-hover'
}`;
export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionProps> = (props) => {
const { modalProps, state } = props;
const setupStrategy = () =>
@ -51,6 +58,34 @@ export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionPro
<p class="mt-1 text-xs leading-5 text-muted">{setupStrategy().detail}</p>
</div>
);
const setupModeControls = () => (
<div class="flex gap-2 flex-wrap">
<button
type="button"
onClick={() => state.updateField('setupMode', 'auto')}
class={setupModeButtonClass(state.formData().setupMode === 'auto')}
>
API Inventory
<span class="ml-1.5 px-1.5 py-0.5 text-[10px] font-semibold bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded">
Recommended
</span>
</button>
<button
type="button"
onClick={() => state.updateField('setupMode', 'agent')}
class={setupModeButtonClass(state.formData().setupMode === 'agent')}
>
Host Telemetry Agent
</button>
<button
type="button"
onClick={() => state.updateField('setupMode', 'manual')}
class={setupModeButtonClass(state.isAdvancedSetupMode())}
>
Manual Token Setup
</button>
</div>
);
return (
<div class="space-y-4">
@ -74,77 +109,22 @@ export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionPro
{setupStrategyPanel()}
<div class="space-y-3 text-xs">
<div class="flex gap-2 flex-wrap">
<button
type="button"
onClick={() => state.updateField('setupMode', 'agent')}
class={`inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md border border-transparent transition-colors ${
state.formData().setupMode === 'agent'
? 'bg-surface text-blue-600 dark:text-blue-300 border-border shadow-sm'
: 'text-muted hover:text-blue-600 dark:hover:text-blue-300 hover:bg-surface-hover'
}`}
>
Agent Install
<span class="ml-1.5 px-1.5 py-0.5 text-[10px] font-semibold bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded">
Recommended
</span>
</button>
<button
type="button"
onClick={() => {
if (state.formData().setupMode === 'agent') {
state.updateField('setupMode', 'auto');
}
}}
class={`inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md border border-transparent transition-colors ${
state.isAdvancedSetupMode()
? 'bg-surface text-blue-600 dark:text-blue-300 border-border shadow-sm'
: 'text-muted hover:text-blue-600 dark:hover:text-blue-300 hover:bg-surface-hover'
}`}
>
Advanced
</button>
</div>
<Show when={state.isAdvancedSetupMode()}>
<div class="mt-1 flex gap-2 flex-wrap pl-0.5">
<button
type="button"
onClick={() => state.updateField('setupMode', 'auto')}
class={`inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-md border border-transparent transition-colors ${
state.formData().setupMode === 'auto'
? 'bg-surface text-blue-600 dark:text-blue-300 border-border shadow-sm'
: 'text-muted hover:text-blue-600 dark:hover:text-blue-300 hover:bg-surface-hover'
}`}
>
Direct Connection
</button>
<button
type="button"
onClick={() => state.updateField('setupMode', 'manual')}
class={`inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-md border border-transparent transition-colors ${
state.formData().setupMode === 'manual'
? 'bg-surface text-blue-600 dark:text-blue-300 border-border shadow-sm'
: 'text-muted hover:text-blue-600 dark:hover:text-blue-300 hover:bg-surface-hover'
}`}
>
Manual Token Setup
</button>
</div>
</Show>
{setupModeControls()}
<Show when={state.formData().setupMode === 'agent'}>
<div class="space-y-3">
<p class="text-xs text-muted">
Recommended API + Agent setup. This single command creates the API token,
installs Pulse Agent, registers the node, and leaves Pulse waiting for the agent
check-in:
Optional full host telemetry setup. This command creates the API token, installs
the Pulse Agent root service, registers the node, and leaves Pulse waiting for
the agent check-in:
</p>
<ul class="text-xs text-muted list-disc list-inside space-y-1">
<li>Creates monitoring user and API token automatically</li>
<li>Registers the node with Pulse</li>
<li>Enables temperature monitoring (no SSH required)</li>
<li>Enables Pulse Patrol automation for managing VMs/containers</li>
<li>
Adds host-local telemetry such as temperatures, SMART, ZFS, Ceph, and mdadm
</li>
<li>Enables Pulse Patrol command execution for VMs and containers</li>
</ul>
<p class="text-blue-800 dark:text-blue-200 font-medium">
Run this command on your Proxmox VE node:
@ -217,11 +197,12 @@ export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionPro
<Show when={state.formData().setupMode === 'auto'}>
<div class="space-y-3">
<div class="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 dark:border-amber-700 dark:bg-amber-900">
<p class="text-xs text-amber-800 dark:text-amber-200">
<strong>Advanced API inventory path:</strong> this connects Pulse to Proxmox
without installing the unified agent, so temperature monitoring and Pulse
Patrol automation stay unavailable until an agent is installed.
<div class="rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 dark:border-emerald-800 dark:bg-emerald-950/30">
<p class="text-xs text-emerald-800 dark:text-emerald-200">
<strong>Recommended API inventory path:</strong> this connects Pulse to
Proxmox without installing a root agent. Add the host telemetry agent later
only where you need temperatures, SMART, local storage details, or
agent-driven operations.
</p>
</div>
<p class="text-blue-800 dark:text-blue-200">
@ -556,76 +537,21 @@ export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionPro
{setupStrategyPanel()}
<div class="space-y-3 text-xs">
<div class="flex gap-2 flex-wrap">
<button
type="button"
onClick={() => state.updateField('setupMode', 'agent')}
class={`inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium rounded-md border border-transparent transition-colors ${
state.formData().setupMode === 'agent'
? 'bg-surface text-blue-600 dark:text-blue-300 border-border shadow-sm'
: 'text-muted hover:text-blue-600 dark:hover:text-blue-300 hover:bg-surface-hover'
}`}
>
Agent Install
<span class="text-[10px] px-1.5 py-0.5 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded">
Recommended
</span>
</button>
<button
type="button"
onClick={() => {
if (state.formData().setupMode === 'agent') {
state.updateField('setupMode', 'auto');
}
}}
class={`inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md border border-transparent transition-colors ${
state.isAdvancedSetupMode()
? 'bg-surface text-blue-600 dark:text-blue-300 border-border shadow-sm'
: 'text-muted hover:text-blue-600 dark:hover:text-blue-300 hover:bg-surface-hover'
}`}
>
Advanced
</button>
</div>
<Show when={state.isAdvancedSetupMode()}>
<div class="mt-1 flex gap-2 flex-wrap pl-0.5">
<button
type="button"
onClick={() => state.updateField('setupMode', 'auto')}
class={`inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-md border border-transparent transition-colors ${
state.formData().setupMode === 'auto'
? 'bg-surface text-blue-600 dark:text-blue-300 border-border shadow-sm'
: 'text-muted hover:text-blue-600 dark:hover:text-blue-300 hover:bg-surface-hover'
}`}
>
Direct Connection
</button>
<button
type="button"
onClick={() => state.updateField('setupMode', 'manual')}
class={`inline-flex items-center px-2.5 py-1 text-xs font-medium rounded-md border border-transparent transition-colors ${
state.formData().setupMode === 'manual'
? 'bg-surface text-blue-600 dark:text-blue-300 border-border shadow-sm'
: 'text-muted hover:text-blue-600 dark:hover:text-blue-300 hover:bg-surface-hover'
}`}
>
Manual Token Setup
</button>
</div>
</Show>
{setupModeControls()}
<Show when={state.formData().setupMode === 'agent'}>
<div class="space-y-3">
<p class="text-xs text-muted">
Recommended API + Agent setup. This single command creates the API token,
installs Pulse Agent, registers the server, and leaves Pulse waiting for the
agent check-in:
Optional full host telemetry setup. This command creates the API token, installs
the Pulse Agent root service, registers the server, and leaves Pulse waiting for
the agent check-in:
</p>
<ul class="text-xs text-muted list-disc list-inside space-y-1">
<li>One-command setup (creates API user and token automatically)</li>
<li>Built-in temperature monitoring (no SSH required)</li>
<li>Pulse features (execute commands via Pulse Assistant)</li>
<li>
Host-local telemetry such as temperatures, SMART, services, and local disks
</li>
<li>Agent-driven commands and service telemetry when explicitly used</li>
<li>Automatic reconnection on network issues</li>
</ul>
<p class="text-blue-800 dark:text-blue-200 text-xs mt-3">
@ -699,11 +625,12 @@ export const NodeModalSetupGuideSection: Component<NodeModalSetupGuideSectionPro
<Show when={state.formData().setupMode === 'auto'}>
<div class="space-y-3">
<div class="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 dark:border-amber-700 dark:bg-amber-900">
<p class="text-xs text-amber-800 dark:text-amber-200">
<strong>Advanced API inventory path:</strong> this connects Pulse to Proxmox
Backup Server without installing the unified agent, so host-local telemetry
and agent-driven operations stay unavailable until an agent is installed.
<div class="rounded-md border border-emerald-200 bg-emerald-50 px-3 py-2 dark:border-emerald-800 dark:bg-emerald-950/30">
<p class="text-xs text-emerald-800 dark:text-emerald-200">
<strong>Recommended API inventory path:</strong> this connects Pulse to
Proxmox Backup Server without installing a root agent. Add the host telemetry
agent later only where you need host-local telemetry or agent-driven
operations.
</p>
</div>
<p class="text-blue-800 dark:text-blue-200">

View file

@ -298,7 +298,7 @@ describe('InfrastructureWorkspace', () => {
expect(within(readiness).getByText('Discovery off')).toBeInTheDocument();
expect(within(readiness).getByRole('button', { name: /Install agents/i })).toBeInTheDocument();
expect(screen.getByText('Proxmox VE')).toBeInTheDocument();
expect(screen.getByText('Proxmox VE').closest('tr')?.className).toContain('bg-base');
expect(screen.getByText('Proxmox VE').closest('tr')?.className).toContain('grouped-table-row');
expect(screen.queryByText('VMware vCenter')).toBeNull();
expect(screen.queryByText('TrueNAS SCALE')).toBeNull();
expect(screen.queryByText('Standalone hosts')).toBeNull();
@ -621,7 +621,7 @@ describe('InfrastructureWorkspace', () => {
expect(within(dialog).getByText('Virtualization')).toBeInTheDocument();
expect(within(dialog).getAllByText('API inventory').length).toBeGreaterThan(0);
expect(within(dialog).getByText('Agent telemetry')).toBeInTheDocument();
expect(within(dialog).getAllByText('API + Agent').length).toBeGreaterThan(0);
expect(within(dialog).getAllByText('API first').length).toBeGreaterThan(0);
expect(within(dialog).getAllByText('Unlocks').length).toBeGreaterThan(0);
expect(within(dialog).getByRole('button', { name: /TrueNAS SCALE/i })).toBeInTheDocument();
});

View file

@ -7,7 +7,7 @@ import { NodeModalSetupGuideSection } from '../NodeModalSetupGuideSection';
const renderSetupGuide = (nodeType: NodeModalNodeType = 'pve') => {
const Harness = () => {
const [setupMode, setSetupMode] = createSignal<NodeModalSetupMode>('agent');
const [setupMode, setSetupMode] = createSignal<NodeModalSetupMode>('auto');
const updateField = vi.fn((field: string, value: string | boolean | number) => {
if (field === 'setupMode') {
setSetupMode(value as NodeModalSetupMode);
@ -22,7 +22,7 @@ const renderSetupGuide = (nodeType: NodeModalNodeType = 'pve') => {
copyQuickSetupCommand: vi.fn(),
downloadProxmoxSetupScript: vi.fn(),
formData: () => ({ setupMode: setupMode(), host: '' }),
isAdvancedSetupMode: () => setupMode() === 'auto' || setupMode() === 'manual',
isAdvancedSetupMode: () => setupMode() === 'manual',
loadingAgentCommand: () => false,
quickSetupExpiry: () => null,
quickSetupExpiryLabel: () => '',
@ -40,22 +40,30 @@ const renderSetupGuide = (nodeType: NodeModalNodeType = 'pve') => {
describe('NodeModalSetupGuideSection', () => {
afterEach(() => cleanup());
it('keeps Proxmox Agent install framed as the recommended API plus Agent path', () => {
it('keeps Proxmox API inventory framed as the recommended least-privilege path', () => {
renderSetupGuide('pve');
expect(screen.getByText('Source strategy')).toBeInTheDocument();
expect(screen.getByText('API + Agent')).toBeInTheDocument();
expect(screen.getByText(/No token fields are needed in this form/i)).toBeInTheDocument();
expect(screen.getByText(/No token fields are needed here/i)).toBeInTheDocument();
expect(screen.getByText('API inventory')).toBeInTheDocument();
expect(screen.getByText(/Recommended least-privilege path/i)).toBeInTheDocument();
expect(screen.getByText(/Recommended API inventory path/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /API Inventory/i })).toBeInTheDocument();
expect(screen.queryByText('Manual API token')).not.toBeInTheDocument();
});
it('labels Proxmox direct and manual setup as advanced escape hatches', () => {
it('keeps the root agent path explicit as optional full host telemetry', () => {
renderSetupGuide('pve');
fireEvent.click(screen.getByRole('button', { name: /^Advanced$/i }));
expect(screen.getByText('API inventory')).toBeInTheDocument();
expect(screen.getByText(/Advanced API inventory path/i)).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /^Host Telemetry Agent$/i }));
expect(screen.getByText('Host telemetry agent')).toBeInTheDocument();
expect(screen.getAllByText(/Optional full host telemetry/i).length).toBeGreaterThan(0);
expect(screen.getAllByText(/Pulse Agent root service/i).length).toBeGreaterThan(0);
expect(screen.getByText(/temperatures, SMART, ZFS, Ceph, and mdadm/i)).toBeInTheDocument();
});
it('labels manual setup as the advanced token escape hatch', () => {
renderSetupGuide('pve');
fireEvent.click(screen.getByRole('button', { name: /^Manual Token Setup$/i }));
expect(screen.getByText('Manual API token')).toBeInTheDocument();
@ -63,11 +71,11 @@ describe('NodeModalSetupGuideSection', () => {
expect(screen.getByText(/Advanced escape hatch: use this only when/i)).toBeInTheDocument();
});
it('applies the same API plus Agent strategy language to PBS', () => {
it('applies the same API-first strategy language to PBS', () => {
renderSetupGuide('pbs');
expect(screen.getByText('API + Agent')).toBeInTheDocument();
expect(screen.getByText('API inventory')).toBeInTheDocument();
expect(screen.getByText(/creates the Proxmox Backup Server API token/i)).toBeInTheDocument();
expect(screen.getByText(/No token fields are needed here/i)).toBeInTheDocument();
expect(screen.getAllByText(/without installing a root agent/i).length).toBeGreaterThan(0);
});
});

View file

@ -530,7 +530,7 @@ describe('settings architecture guardrails', () => {
expect(infrastructureOnboardingPresentationSource).toContain(
'getInfrastructureSourceStrategyPresentation',
);
expect(infrastructureOnboardingPresentationSource).toContain('API + Agent');
expect(infrastructureOnboardingPresentationSource).toContain('API first');
expect(infrastructureOnboardingPresentationSource).toContain(
'getInfrastructureSupportSummaryBadges',
);
@ -549,10 +549,10 @@ describe('settings architecture guardrails', () => {
);
expect(nodeModalAuthenticationSectionSource).toContain('Advanced manual token path');
expect(nodeModalSetupGuideSectionSource).toContain('Source strategy');
expect(nodeModalSetupGuideSectionSource).toContain('API + Agent');
expect(nodeModalSetupGuideSectionSource).toContain('Host telemetry agent');
expect(nodeModalSetupGuideSectionSource).toContain('API inventory');
expect(nodeModalSetupGuideSectionSource).toContain('Manual API token');
expect(nodeModalSetupGuideSectionSource).toContain('No token fields are needed');
expect(nodeModalSetupGuideSectionSource).toContain('Recommended least-privilege path');
expect(nodeModalStatusFooterSource).toContain('guidedSetupOnlyMode');
expect(nodeModalStateSource).toContain('data.setupMode !==');

View file

@ -41,8 +41,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
const [loadingAgentCommand, setLoadingAgentCommand] = createSignal(false);
const [agentCommandError, setAgentCommandError] = createSignal<string | null>(null);
const isAdvancedSetupMode = () =>
formData().setupMode === 'auto' || formData().setupMode === 'manual';
const isAdvancedSetupMode = () => formData().setupMode === 'manual';
const showTemperatureMonitoringSection = () =>
typeof props.temperatureMonitoringEnabled === 'boolean';
const temperatureMonitoringEnabledValue = () => props.temperatureMonitoringEnabled ?? true;
@ -92,7 +91,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
setAgentCommandError(copyFailureMessage);
notificationStore.error(copyFailureMessage);
} catch (error) {
logger.error('[Agent Install] Error:', error);
logger.error('[Host Telemetry Agent] Error:', error);
const message = error instanceof Error ? error.message : 'Failed to generate install command';
setAgentCommandError(message);
notificationStore.error(message);
@ -337,7 +336,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
data.setupMode !== 'manual'
) {
const setupPath =
data.setupMode === 'agent' ? 'Pulse Agent install command' : 'setup command';
data.setupMode === 'agent' ? 'Host Telemetry Agent command' : 'API Inventory setup command';
notificationStore.info(
`Run the ${setupPath} on the host. Pulse will add the node automatically.`,
);

View file

@ -42,7 +42,8 @@ describe('infrastructureOnboardingPresentation', () => {
expect(agent.catalogDescription).toContain('Low-overhead host telemetry');
expect(agent.sourceStrategy).toBe('agent');
expect(pve.sourceStrategy).toBe('api-agent');
expect(pve.coverage).toContain('Install Pulse Agent on each node');
expect(pve.coverage).toContain('through the Proxmox API');
expect(pve.coverage).toContain('Install Pulse Agent only on nodes');
expect(pve.coverage).toContain('SMART data');
});
@ -56,8 +57,8 @@ describe('infrastructureOnboardingPresentation', () => {
summary: 'Pulse Agent',
});
expect(getInfrastructureSourceStrategyPresentation('api-agent')).toMatchObject({
label: 'API + Agent',
summary: 'Platform API with Pulse Agent',
label: 'API first',
summary: 'Platform API, agent optional',
});
expect(getInfrastructureOnboardingProductPresentation('vmware').sourceStrategy).toBe('api');

View file

@ -17,18 +17,18 @@ describe('nodeModalPresentation', () => {
it('returns canonical default form data per proxmox node type', () => {
expect(getNodeModalDefaultFormData('pve')).toMatchObject({
authType: 'token',
setupMode: 'agent',
setupMode: 'auto',
verifySSL: true,
monitorPhysicalDisks: false,
});
expect(getNodeModalDefaultFormData('pbs')).toMatchObject({
authType: 'token',
setupMode: 'agent',
setupMode: 'auto',
verifySSL: true,
});
expect(getNodeModalDefaultFormData('pmg')).toMatchObject({
authType: 'password',
setupMode: 'agent',
setupMode: 'manual',
monitorMailStats: true,
monitorQueues: true,
});

View file

@ -85,10 +85,10 @@ const SOURCE_STRATEGY_PRESENTATION: Record<
detail: 'Installs Pulse Agent for host telemetry, local services, Docker, and Kubernetes.',
},
'api-agent': {
label: 'API + Agent',
summary: 'Platform API with Pulse Agent',
label: 'API first',
summary: 'Platform API, agent optional',
detail:
'Starts from platform inventory and adds Pulse Agent when node-local telemetry is needed.',
'Starts with platform API inventory and adds Pulse Agent only where node-local telemetry is needed.',
},
};
@ -130,7 +130,7 @@ const PRODUCT_PRESENTATION: Record<
label: 'Proxmox VE',
bestFor: 'Virtualization clusters and standalone hypervisors',
coverage:
'VMs, containers, storage, and cluster health. Install Pulse Agent on each node when you want full node-local telemetry such as temperatures and SMART data',
'VMs, containers, storage, and cluster health through the Proxmox API. Install Pulse Agent only on nodes where you want full node-local telemetry such as temperatures and SMART data',
catalogDescription: 'VMs, containers, storage, cluster health',
sourceStrategy: 'api-agent',
autoDetect: true,
@ -140,7 +140,8 @@ const PRODUCT_PRESENTATION: Record<
pbs: {
label: 'Proxmox Backup Server',
bestFor: 'Backup infrastructure and protected storage',
coverage: 'Backup jobs, sync, verify, prune, GC',
coverage:
'Backup jobs, sync, verify, prune, and GC through the Proxmox API. Install Pulse Agent only where host-local telemetry is needed',
catalogDescription: 'Backup jobs, sync, verify, prune, GC',
sourceStrategy: 'api-agent',
autoDetect: true,

View file

@ -41,15 +41,15 @@ export interface NodeModalTestResultPresentation {
icon: NodeModalTestStatus;
}
export function getNodeModalDefaultFormData(
nodeType: NodeModalNodeType,
): NodeModalFormData {
export function getNodeModalDefaultFormData(nodeType: NodeModalNodeType): NodeModalFormData {
const setupMode: NodeModalSetupMode = nodeType === 'pmg' ? 'manual' : 'auto';
return {
name: '',
host: '',
guestURL: '',
authType: nodeType === 'pmg' ? 'password' : 'token',
setupMode: 'agent',
setupMode,
user: '',
password: '',
tokenName: '',