mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Make Proxmox onboarding API-first
This commit is contained in:
parent
cf103ca9fe
commit
c61ea4947a
16 changed files with 214 additions and 197 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 !==');
|
||||
|
||||
|
|
|
|||
|
|
@ -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.`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue