- {props.pveNodes().length}
+
+
+
+
+
+
Direct Proxmox connections
+
+ Review fallback direct coverage separately from agent-managed hosts.
+
+
+ Manage direct connections
+
-
-
PBS
-
- {props.pbsNodes().length}
+
+
+
+
PVE
+
+ {props.pveNodes().length}
+
-
-
-
PMG
-
- {props.pmgNodes().length}
+
+
PBS
+
+ {props.pbsNodes().length}
+
+
+
+
PMG
+
+ {props.pmgNodes().length}
+
-
-
+
+
+
+
-
-
-
-);
+ );
+};
export default InfrastructureReportingPanel;
diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx
index 9d5441265..42a6fabf3 100644
--- a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx
@@ -14,26 +14,18 @@ vi.mock('@solidjs/router', async () => {
};
});
-vi.mock('../InfrastructureOperationsController', () => ({
- InfrastructureOperationsController: (props: { showInventory?: boolean; showInstaller?: boolean }) => (
-
- {props.showInventory === false
- ? 'install'
- : props.showInstaller === false
- ? 'inventory'
- : 'default'}
-
- ),
+vi.mock('../InfrastructureInstallPanel', () => ({
+ InfrastructureInstallPanel: () =>
install
,
+}));
+
+vi.mock('../InfrastructureReportingPanel', () => ({
+ InfrastructureReportingPanel: () =>
profiles
,
}));
vi.mock('../ProxmoxSettingsPanel', () => ({
ProxmoxSettingsPanel: () =>
direct
,
}));
-vi.mock('../AgentProfilesPanel', () => ({
- AgentProfilesPanel: () =>
profiles
,
-}));
-
describe('InfrastructureWorkspace', () => {
beforeEach(() => {
navigateSpy.mockReset();
diff --git a/frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx b/frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx
index cc4ef3312..bb9f3583b 100644
--- a/frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx
@@ -4,6 +4,10 @@ import { createSignal } from 'solid-js';
import { createStore } from 'solid-js/store';
import { Router, Route } from '@solidjs/router';
import { UnifiedAgents } from '../UnifiedAgents';
+import infrastructureInstallPanelSource from '../InfrastructureInstallPanel.tsx?raw';
+import infrastructureOperationsControllerSource from '../InfrastructureOperationsController.tsx?raw';
+import infrastructureReportingPanelSource from '../InfrastructureReportingPanel.tsx?raw';
+import infrastructureOperationsStateSource from '../useInfrastructureOperationsState.tsx?raw';
import type {
Agent,
ConnectedInfrastructureItem,
@@ -42,6 +46,17 @@ const refetchResourcesMock = vi.fn();
const [mockResources, setMockResources] = createSignal
([]);
let securityStatusResponse = { requiresAuth: true, apiTokenConfigured: false };
+describe('UnifiedAgents ownership guardrails', () => {
+ it('routes controller and workspace panels through the shared infrastructure operations state owner', () => {
+ expect(infrastructureOperationsControllerSource).toContain('useInfrastructureOperationsState');
+ expect(infrastructureInstallPanelSource).toContain('useInfrastructureOperationsState');
+ expect(infrastructureReportingPanelSource).toContain('useInfrastructureOperationsState');
+ expect(infrastructureOperationsStateSource).toContain('renderInstallerSection');
+ expect(infrastructureOperationsStateSource).toContain('renderInventorySection');
+ expect(infrastructureOperationsStateSource).toContain('renderStopMonitoringDialog');
+ });
+});
+
vi.mock('@/App', () => ({
useWebSocket: () => mockWsStore,
}));
diff --git a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts
index a0f5cc25a..8dc9a7bde 100644
--- a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts
+++ b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';
import agentProfilesPanelSource from '../AgentProfilesPanel.tsx?raw';
import apiTokenManagerSource from '../APITokenManager.tsx?raw';
-import infrastructureOperationsControllerSource from '../InfrastructureOperationsController.tsx?raw';
+import infrastructureOperationsStateSource from '../useInfrastructureOperationsState.tsx?raw';
import agentLedgerPanelSource from '../MonitoredSystemLedgerPanel.tsx?raw';
import alertsPageSource from '@/pages/Alerts.tsx?raw';
import setupCompletionPanelSource from '@/components/SetupWizard/SetupCompletionPanel.tsx?raw';
@@ -156,43 +156,43 @@ describe('monitored-system model guardrails', () => {
});
it('keeps UnifiedAgents free of v5 merge-workaround patterns', () => {
- expect(infrastructureOperationsControllerSource).not.toContain('previousHostTypes');
- expect(infrastructureOperationsControllerSource).not.toContain('const allHosts = createMemo(');
- expect(infrastructureOperationsControllerSource).toContain('@/utils/unifiedAgentStatusPresentation');
- expect(infrastructureOperationsControllerSource).not.toContain('const MONITORING_STOPPED_STATUS_LABEL =');
- expect(infrastructureOperationsControllerSource).not.toContain('const ALLOW_RECONNECT_LABEL =');
- expect(infrastructureOperationsControllerSource).toContain('withPrivilegeEscalation');
- expect(infrastructureOperationsControllerSource).toContain('@/utils/agentCapabilityPresentation');
- expect(infrastructureOperationsControllerSource).not.toContain('const getCapabilityLabel =');
- expect(infrastructureOperationsControllerSource).not.toContain('const getCapabilityBadgeClass =');
+ expect(infrastructureOperationsStateSource).not.toContain('previousHostTypes');
+ expect(infrastructureOperationsStateSource).not.toContain('const allHosts = createMemo(');
+ expect(infrastructureOperationsStateSource).toContain('@/utils/unifiedAgentStatusPresentation');
+ expect(infrastructureOperationsStateSource).not.toContain('const MONITORING_STOPPED_STATUS_LABEL =');
+ expect(infrastructureOperationsStateSource).not.toContain('const ALLOW_RECONNECT_LABEL =');
+ expect(infrastructureOperationsStateSource).toContain('withPrivilegeEscalation');
+ expect(infrastructureOperationsStateSource).toContain('@/utils/agentCapabilityPresentation');
+ expect(infrastructureOperationsStateSource).not.toContain('const getCapabilityLabel =');
+ expect(infrastructureOperationsStateSource).not.toContain('const getCapabilityBadgeClass =');
expect(agentCapabilityPresentationSource).toContain("export type AgentCapability = 'agent'");
expect(agentCapabilityPresentationSource).toContain('export function getAgentCapabilityLabel');
expect(agentCapabilityPresentationSource).toContain(
'export function getAgentCapabilityBadgeClass',
);
- expect(infrastructureOperationsControllerSource).not.toContain('isConnectedHealthStatus');
- expect(infrastructureOperationsControllerSource).not.toContain('const connectedFromStatus =');
+ expect(infrastructureOperationsStateSource).not.toContain('isConnectedHealthStatus');
+ expect(infrastructureOperationsStateSource).not.toContain('const connectedFromStatus =');
expect(agentProfilesPanelSource).toContain('isConnectedHealthStatus');
expect(agentProfilesPanelSource).not.toContain('const connectedFromStatus =');
expect(statusUtilsSource).toContain('export function isConnectedHealthStatus');
- expect(infrastructureOperationsControllerSource).toContain('@/utils/unifiedAgentStatusPresentation');
- expect(infrastructureOperationsControllerSource).not.toContain('const statusBadgeClass =');
- expect(infrastructureOperationsControllerSource).not.toContain('const statusBadgeClasses =');
- expect(infrastructureOperationsControllerSource).toContain('getUnifiedAgentLookupStatusPresentation');
+ expect(infrastructureOperationsStateSource).toContain('@/utils/unifiedAgentStatusPresentation');
+ expect(infrastructureOperationsStateSource).not.toContain('const statusBadgeClass =');
+ expect(infrastructureOperationsStateSource).not.toContain('const statusBadgeClasses =');
+ expect(infrastructureOperationsStateSource).toContain('getUnifiedAgentLookupStatusPresentation');
expect(unifiedAgentStatusPresentationSource).toContain(
'export function getUnifiedAgentStatusPresentation',
);
expect(unifiedAgentStatusPresentationSource).toContain(
'export function getUnifiedAgentLookupStatusPresentation',
);
- expect(infrastructureOperationsControllerSource).toContain('getInventorySubjectLabel');
- expect(infrastructureOperationsControllerSource).toContain('getMonitoringStoppedEmptyState');
- expect(infrastructureOperationsControllerSource).toContain('getRemovedUnifiedAgentItemLabel');
- expect(infrastructureOperationsControllerSource).toContain('getUnifiedAgentLastSeenLabel');
- expect(infrastructureOperationsControllerSource).not.toContain('const getInventorySubjectLabel =');
- expect(infrastructureOperationsControllerSource).not.toContain('const getRemovedItemLabel =');
- expect(infrastructureOperationsControllerSource).not.toContain('const lastSeenLabel = () => {');
- expect(infrastructureOperationsControllerSource).not.toContain(
+ expect(infrastructureOperationsStateSource).toContain('getInventorySubjectLabel');
+ expect(infrastructureOperationsStateSource).toContain('getMonitoringStoppedEmptyState');
+ expect(infrastructureOperationsStateSource).toContain('getRemovedUnifiedAgentItemLabel');
+ expect(infrastructureOperationsStateSource).toContain('getUnifiedAgentLastSeenLabel');
+ expect(infrastructureOperationsStateSource).not.toContain('const getInventorySubjectLabel =');
+ expect(infrastructureOperationsStateSource).not.toContain('const getRemovedItemLabel =');
+ expect(infrastructureOperationsStateSource).not.toContain('const lastSeenLabel = () => {');
+ expect(infrastructureOperationsStateSource).not.toContain(
'No monitoring-stopped items match the current filters.',
);
expect(unifiedAgentInventoryPresentationSource).toContain(
@@ -225,15 +225,15 @@ describe('monitored-system model guardrails', () => {
expect(unifiedAgentInventoryPresentationSource).toContain(
'export function getUnifiedAgentClipboardCopySuccessMessage',
);
- expect(infrastructureOperationsControllerSource).not.toContain(
+ expect(infrastructureOperationsStateSource).not.toContain(
'No host identifiers are available to stop monitoring.',
);
- expect(infrastructureOperationsControllerSource).not.toContain(
+ expect(infrastructureOperationsStateSource).not.toContain(
'Failed to update agent configuration',
);
- expect(infrastructureOperationsControllerSource).not.toContain('Uninstall command copied');
- expect(infrastructureOperationsControllerSource).not.toContain('Upgrade command copied');
- expect(infrastructureOperationsControllerSource).not.toContain(
+ expect(infrastructureOperationsStateSource).not.toContain('Uninstall command copied');
+ expect(infrastructureOperationsStateSource).not.toContain('Upgrade command copied');
+ expect(infrastructureOperationsStateSource).not.toContain(
"notificationStore.error('Failed to copy')",
);
expect(relaySettingsPanelSource).toContain('getRelayConnectionPresentation');
@@ -255,11 +255,11 @@ describe('monitored-system model guardrails', () => {
expect(relayOnboardingCardSource).toContain('RELAY_ONBOARDING_DISCONNECTED_LABEL');
expect(relayOnboardingCardSource).not.toContain('Pair Your Mobile Device');
expect(relayOnboardingCardSource).not.toContain('Relay is currently disconnected.');
- expect(infrastructureOperationsControllerSource).toContain('STORAGE_KEYS.SETUP_HANDOFF');
- expect(infrastructureOperationsControllerSource).toContain(
+ expect(infrastructureOperationsStateSource).toContain('STORAGE_KEYS.SETUP_HANDOFF');
+ expect(infrastructureOperationsStateSource).toContain(
'Security configured. Save these first-run credentials now.',
);
- expect(infrastructureOperationsControllerSource).toContain(
+ expect(infrastructureOperationsStateSource).toContain(
'Generate a scoped install token below before copying agent commands.',
);
expect(setupCompletionPanelSource).toContain('@/utils/relayPresentation');
diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts
index caaa04ac3..59ffcedf5 100644
--- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts
+++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts
@@ -5,6 +5,7 @@ import infrastructureWorkspaceSource from '../InfrastructureWorkspace.tsx?raw';
import infrastructureInstallPanelSource from '../InfrastructureInstallPanel.tsx?raw';
import infrastructureOperationsControllerSource from '../InfrastructureOperationsController.tsx?raw';
import infrastructureReportingPanelSource from '../InfrastructureReportingPanel.tsx?raw';
+import infrastructureOperationsStateSource from '../useInfrastructureOperationsState.tsx?raw';
import apiAccessPanelSource from '../APIAccessPanel.tsx?raw';
import auditLogPanelSource from '../AuditLogPanel.tsx?raw';
import auditWebhookPanelSource from '../AuditWebhookPanel.tsx?raw';
@@ -38,6 +39,7 @@ const extractedModules = [
'../settingsFeatureGates.ts',
'../BackupTransferDialogs.tsx',
'../InfrastructureOperationsController.tsx',
+ '../useInfrastructureOperationsState.tsx',
'../InfrastructureWorkspace.tsx',
'../InfrastructureInstallPanel.tsx',
'../InfrastructureReportingPanel.tsx',
@@ -277,9 +279,10 @@ describe('Settings architecture guardrails', () => {
expect(infrastructureWorkspaceSource).not.toContain('tracking-[0.22em]');
expect(infrastructureWorkspaceSource).toContain('InfrastructureInstallPanel');
expect(infrastructureWorkspaceSource).toContain('InfrastructureReportingPanel');
- expect(infrastructureInstallPanelSource).toContain('InfrastructureOperationsController');
- expect(infrastructureReportingPanelSource).toContain('InfrastructureOperationsController');
- expect(infrastructureOperationsControllerSource).toContain('export const InfrastructureOperationsController');
+ expect(infrastructureInstallPanelSource).toContain('useInfrastructureOperationsState');
+ expect(infrastructureReportingPanelSource).toContain('useInfrastructureOperationsState');
+ expect(infrastructureOperationsControllerSource).toContain('useInfrastructureOperationsState');
+ expect(infrastructureOperationsStateSource).toContain('export const useInfrastructureOperationsState');
expect(infrastructureInstallPanelSource).not.toContain(' {
+ const now = new Date();
+ const iso = now.toISOString().slice(0, 16); // YYYY-MM-DDTHH:MM
+ const stamp = iso.replace('T', ' ').replace(/:/g, '-');
+ return `Agent ${stamp}`;
+};
+
+const normalizeTelemetryPart = (value: string) =>
+ value
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '_')
+ .replace(/^_+|_+$/g, '');
+
+const shellQuoteArg = (value: string) => `'${value.replace(/'/g, `'\"'\"'`)}'`;
+type AgentPlatform = 'linux' | 'macos' | 'freebsd' | 'windows';
+type UnifiedAgentStatus = 'active' | 'removed';
+type ScopeCategory = 'default' | 'profile' | 'ai-managed' | 'na';
+type InstallProfile = 'auto' | 'docker' | 'kubernetes' | 'proxmox-pve' | 'proxmox-pbs' | 'truenas';
+
+type SetupHandoffState = {
+ username: string;
+ password: string;
+ apiToken: string;
+ createdAt?: string;
+};
+
+type UnifiedAgentRow = {
+ rowKey: string;
+ id: string;
+ agentActionId?: string;
+ dockerActionId?: string;
+ kubernetesActionId?: string;
+ name: string;
+ hostname?: string;
+ displayName?: string;
+ capabilities: AgentCapability[];
+ status: UnifiedAgentStatus;
+ healthStatus?: string;
+ lastSeen?: number;
+ removedAt?: number;
+ version?: string;
+ isOutdatedBinary?: boolean;
+ linkedNodeId?: string;
+ commandsEnabled?: boolean;
+ agentId?: string;
+ upgradePlatform: AgentPlatform;
+ scope: {
+ label: string;
+ detail?: string;
+ category: ScopeCategory;
+ };
+ installFlags: string[];
+ searchText: string;
+ kubernetesInfo?: {
+ server?: string;
+ context?: string;
+ tokenName?: string;
+ };
+ surfaces: Array<{
+ key: string;
+ kind: AgentCapability;
+ label: string;
+ detail: string;
+ idLabel?: string;
+ idValue?: string;
+ action?: 'stop-monitoring' | 'allow-reconnect';
+ controlId?: string;
+ }>;
+};
+
+type InventoryActionType = 'stop-monitoring' | 'allow-reconnect';
+
+type InventoryActionNotice = {
+ tone: 'success' | 'info';
+ title: string;
+ detail: string;
+ showRecoveryQueueLink?: boolean;
+};
+
+type StopMonitoringDialogState = {
+ row: UnifiedAgentRow;
+ subject: string;
+ scopeLabel: string;
+};
+
+const getCapabilitySurfaceLabel = (capability: AgentCapability) => {
+ switch (capability) {
+ case 'agent':
+ return 'Host telemetry';
+ case 'docker':
+ return 'Docker runtime data';
+ case 'kubernetes':
+ return 'Kubernetes cluster data';
+ case 'proxmox':
+ return 'Proxmox data';
+ case 'pbs':
+ return 'PBS data';
+ case 'pmg':
+ return 'PMG data';
+ default:
+ return getAgentCapabilityLabel(capability);
+ }
+};
+
+const getReconnectActionLabel = (row: UnifiedAgentRow) => {
+ if (row.capabilities.includes('docker')) {
+ return 'Allow Docker reconnect';
+ }
+ if (row.capabilities.includes('kubernetes')) {
+ return 'Allow Kubernetes reconnect';
+ }
+ return 'Allow host reconnect';
+};
+
+const joinHumanList = (parts: string[]) => {
+ if (parts.length === 0) return '';
+ if (parts.length === 1) return parts[0];
+ if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
+ return `${parts.slice(0, -1).join(', ')}, and ${parts.at(-1)}`;
+};
+
+const sentenceCaseSurfaceLabel = (label: string, index: number) => {
+ if (index !== 0 || label.length === 0) {
+ return label;
+ }
+ return `${label.slice(0, 1).toLowerCase()}${label.slice(1)}`;
+};
+
+const getRowReportingSummary = (row: UnifiedAgentRow) => {
+ const reported = row.surfaces.map((surface, index) => sentenceCaseSurfaceLabel(surface.label, index));
+ if (reported.length === 0) {
+ return '';
+ }
+ return `Pulse is receiving ${joinHumanList(reported)} from this item.`;
+};
+
+const getRowSurfaceBreakdown = (row: UnifiedAgentRow) => {
+ return row.surfaces;
+};
+
+const getStopMonitoringSurfaces = (row: UnifiedAgentRow) => {
+ const surfaces = getRowSurfaceBreakdown(row);
+ const stopMonitoringSurfaces = surfaces.filter((surface) => surface.action === 'stop-monitoring');
+ const hostManagedStopApplies = stopMonitoringSurfaces.some((surface) => surface.kind === 'agent');
+ if (!hostManagedStopApplies) {
+ return stopMonitoringSurfaces;
+ }
+ return surfaces.filter((surface) =>
+ ['agent', 'docker', 'kubernetes', 'proxmox', 'pbs', 'pmg'].includes(surface.kind),
+ );
+};
+
+const getStopMonitoringScopeLabel = (row: UnifiedAgentRow) => {
+ const surfaceLabels = getStopMonitoringSurfaces(row).map((surface) => surface.label);
+ if (surfaceLabels.length === 0) {
+ return 'Reporting for this item';
+ }
+ return joinHumanList(surfaceLabels);
+};
+
+const createSurfaceScopedRow = (
+ row: UnifiedAgentRow,
+ surfaceKey: 'agent' | 'docker' | 'kubernetes' | 'proxmox' | 'pbs' | 'pmg',
+): UnifiedAgentRow => {
+ if (surfaceKey === 'docker') {
+ return {
+ ...row,
+ rowKey: `${row.rowKey}-docker-surface`,
+ capabilities: ['docker'],
+ agentActionId: undefined,
+ kubernetesActionId: undefined,
+ linkedNodeId: undefined,
+ surfaces: row.surfaces.filter((surface) => surface.kind === 'docker'),
+ };
+ }
+
+ if (surfaceKey === 'kubernetes') {
+ return {
+ ...row,
+ rowKey: `${row.rowKey}-kubernetes-surface`,
+ capabilities: ['kubernetes'],
+ agentActionId: undefined,
+ dockerActionId: undefined,
+ linkedNodeId: undefined,
+ surfaces: row.surfaces.filter((surface) => surface.kind === 'kubernetes'),
+ };
+ }
+
+ if (surfaceKey === 'agent') {
+ const hostManagedCapabilities: AgentCapability[] = ['agent'];
+ if (row.capabilities.includes('proxmox')) hostManagedCapabilities.push('proxmox');
+ if (row.capabilities.includes('pbs')) hostManagedCapabilities.push('pbs');
+ if (row.capabilities.includes('pmg')) hostManagedCapabilities.push('pmg');
+ return {
+ ...row,
+ rowKey: `${row.rowKey}-agent-surface`,
+ capabilities: hostManagedCapabilities,
+ dockerActionId: undefined,
+ kubernetesActionId: undefined,
+ surfaces: row.surfaces.filter((surface) =>
+ ['agent', 'proxmox', 'pbs', 'pmg'].includes(surface.kind),
+ ),
+ };
+ }
+
+ if (surfaceKey === 'pbs') {
+ return {
+ ...row,
+ rowKey: `${row.rowKey}-pbs-surface`,
+ capabilities: ['pbs'],
+ dockerActionId: undefined,
+ kubernetesActionId: undefined,
+ surfaces: row.surfaces.filter((surface) => surface.kind === 'pbs'),
+ };
+ }
+
+ if (surfaceKey === 'pmg') {
+ return {
+ ...row,
+ rowKey: `${row.rowKey}-pmg-surface`,
+ capabilities: ['pmg'],
+ dockerActionId: undefined,
+ kubernetesActionId: undefined,
+ surfaces: row.surfaces.filter((surface) => surface.kind === 'pmg'),
+ };
+ }
+
+ return {
+ ...row,
+ rowKey: `${row.rowKey}-proxmox-surface`,
+ capabilities: ['proxmox'],
+ dockerActionId: undefined,
+ kubernetesActionId: undefined,
+ surfaces: row.surfaces.filter((surface) => surface.kind === 'proxmox'),
+ };
+};
+
+const INSTALL_PROFILE_OPTIONS: {
+ value: InstallProfile;
+ label: string;
+ description: string;
+ flags: string[];
+}[] = [
+ {
+ value: 'auto',
+ label: 'Auto-detect (recommended)',
+ description:
+ 'Let the installer detect Docker, Kubernetes, Proxmox, and agent capabilities automatically.',
+ flags: [],
+ },
+ {
+ value: 'docker',
+ label: 'Docker / Podman runtime',
+ description: 'Force container runtime monitoring even when detection is restricted.',
+ flags: ['--enable-docker', '--disable-host'],
+ },
+ {
+ value: 'kubernetes',
+ label: 'Kubernetes node',
+ description: 'Force Kubernetes monitoring on cluster nodes.',
+ flags: ['--enable-kubernetes'],
+ },
+ {
+ value: 'proxmox-pve',
+ label: 'Proxmox VE node',
+ description: 'Force Proxmox integration and register as a PVE node.',
+ flags: ['--enable-proxmox', '--proxmox-type pve'],
+ },
+ {
+ value: 'proxmox-pbs',
+ label: 'Proxmox Backup node',
+ description: 'Force Proxmox integration and register as a PBS node.',
+ flags: ['--enable-proxmox', '--proxmox-type pbs'],
+ },
+ {
+ value: 'truenas',
+ label: 'TrueNAS SCALE agent',
+ description:
+ 'Use default auto-detection; installer applies TrueNAS-safe service handling automatically.',
+ flags: [],
+ },
+];
+
+// Generate platform-specific commands with the appropriate Pulse URL
+// Uses agentUrl from API (PULSE_PUBLIC_URL) if configured, otherwise falls back to window.location
+const buildCommandsByPlatform = (
+ unixCommand: string,
+ windowsInteractiveCommand: string,
+ windowsParameterizedCommand: string,
+): Record<
+ AgentPlatform,
+ {
+ title: string;
+ description: string;
+ snippets: { label: string; command: string; note?: string | any }[];
+ }
+> => ({
+ linux: {
+ title: 'Install on Linux',
+ description:
+ 'The unified installer downloads the agent binary and configures the appropriate service for your system.',
+ snippets: [
+ {
+ label: 'Install',
+ command: unixCommand,
+ note: (
+
+ Command auto-escalates with sudo when available; otherwise run from a root
+ shell (for example su -). Auto-detects your init system and works on
+ Debian, Ubuntu, Proxmox, Fedora, Alpine, Unraid, Synology, and more.
+
+ ),
+ },
+ ],
+ },
+ macos: {
+ title: 'Install on macOS',
+ description:
+ 'The unified installer downloads the universal binary and sets up a launchd service for background monitoring.',
+ snippets: [
+ {
+ label: 'Install with launchd',
+ command: unixCommand,
+ note: (
+
+ Command auto-escalates with sudo when available; otherwise run from a root
+ shell. Creates /Library/LaunchDaemons/com.pulse.agent.plist and starts the
+ agent automatically.
+
+ ),
+ },
+ ],
+ },
+ freebsd: {
+ title: 'Install on FreeBSD / pfSense / OPNsense',
+ description:
+ 'The unified installer downloads the FreeBSD binary and sets up an rc.d service for background monitoring.',
+ snippets: [
+ {
+ label: 'Install with rc.d',
+ command: unixCommand,
+ note: (
+
+ Run as root. Note: pfSense/OPNsense don't include bash by default.
+ Install it first: pkg install bash. Creates{' '}
+ /usr/local/etc/rc.d/pulse-agent and starts the agent automatically.
+
+ ),
+ },
+ ],
+ },
+ windows: {
+ title: 'Install on Windows',
+ description:
+ 'Run the PowerShell script to install and configure the unified agent as a Windows service with automatic startup.',
+ snippets: [
+ {
+ label: 'Install as Windows Service (PowerShell)',
+ command: windowsInteractiveCommand,
+ note: (
+
+ Run in PowerShell as Administrator. The script will prompt for the Pulse URL and API
+ token, download the agent binary, and install it as a Windows service with automatic
+ startup.
+
+ ),
+ },
+ {
+ label: 'Install with parameters (PowerShell)',
+ command: windowsParameterizedCommand,
+ note: (
+
+ Non-interactive installation. Set environment variables before running to skip prompts.
+
+ ),
+ },
+ ],
+ },
+});
+
+const agentCapabilityFromSurfaceKind = (kind: ConnectedInfrastructureSurface['kind']): AgentCapability => {
+ switch (kind) {
+ case 'agent':
+ case 'docker':
+ case 'kubernetes':
+ case 'proxmox':
+ case 'pbs':
+ case 'pmg':
+ return kind;
+ default:
+ return 'agent';
+ }
+};
+
+const installFlagsForCapabilities = (capabilities: AgentCapability[]) => {
+ const flags = new Set();
+ if (capabilities.includes('docker')) {
+ flags.add('--enable-docker');
+ flags.add('--disable-host');
+ }
+ if (capabilities.includes('kubernetes')) {
+ flags.add('--enable-kubernetes');
+ }
+ if (capabilities.includes('proxmox')) {
+ flags.add('--enable-proxmox');
+ flags.add('--proxmox-type pve');
+ } else if (capabilities.includes('pbs')) {
+ flags.add('--enable-proxmox');
+ flags.add('--proxmox-type pbs');
+ }
+ return Array.from(flags);
+};
+
+const surfaceBreakdownFromConnectedSurface = (surface: ConnectedInfrastructureSurface) => ({
+ key: surface.kind,
+ kind: agentCapabilityFromSurfaceKind(surface.kind),
+ label: surface.label || getCapabilitySurfaceLabel(agentCapabilityFromSurfaceKind(surface.kind)),
+ detail: surface.detail || '',
+ idLabel: surface.idLabel,
+ idValue: surface.idValue,
+ action: surface.action,
+ controlId: surface.controlId,
+});
+
+const rowFromConnectedInfrastructureItem = (
+ item: ConnectedInfrastructureItem,
+ scope: UnifiedAgentRow['scope'],
+): UnifiedAgentRow => {
+ const surfaces = item.surfaces.map(surfaceBreakdownFromConnectedSurface);
+ const capabilities = Array.from(new Set(surfaces.map((surface) => surface.kind)));
+ const agentSurface = item.surfaces.find((surface) => surface.kind === 'agent');
+ const dockerSurface = item.surfaces.find((surface) => surface.kind === 'docker');
+ const kubernetesSurface = item.surfaces.find((surface) => surface.kind === 'kubernetes');
+ const name = item.name || item.displayName || item.hostname || item.id;
+ const rowKey =
+ item.status === 'ignored'
+ ? item.surfaces[0]?.kind === 'docker'
+ ? `removed-docker-${item.id}`
+ : item.surfaces[0]?.kind === 'kubernetes'
+ ? `removed-k8s-${item.id}`
+ : `removed-host-${item.id}`
+ : kubernetesSurface && !agentSurface && !dockerSurface
+ ? `k8s-${kubernetesSurface.controlId || item.id}`
+ : `agent-${item.id}`;
+ return {
+ rowKey,
+ id: item.id,
+ agentActionId: item.uninstallAgentId || agentSurface?.controlId,
+ dockerActionId: dockerSurface?.controlId,
+ kubernetesActionId: kubernetesSurface?.controlId,
+ name,
+ hostname: item.hostname,
+ displayName: item.displayName,
+ capabilities,
+ status: item.status === 'ignored' ? 'removed' : 'active',
+ healthStatus: item.healthStatus,
+ lastSeen: item.lastSeen,
+ removedAt: item.removedAt,
+ version: item.version,
+ isOutdatedBinary: item.isOutdatedBinary,
+ linkedNodeId: item.linkedNodeId,
+ commandsEnabled: item.commandsEnabled,
+ agentId: item.scopeAgentId || item.uninstallAgentId,
+ upgradePlatform: item.upgradePlatform || 'linux',
+ scope,
+ installFlags: installFlagsForCapabilities(capabilities),
+ searchText: [
+ name,
+ item.displayName,
+ item.hostname,
+ item.id,
+ item.scopeAgentId,
+ item.uninstallAgentId,
+ agentSurface?.controlId,
+ dockerSurface?.controlId,
+ kubernetesSurface?.controlId,
+ ]
+ .filter(Boolean)
+ .join(' ')
+ .toLowerCase(),
+ kubernetesInfo:
+ kubernetesSurface || capabilities.includes('kubernetes')
+ ? {
+ server: undefined,
+ context: undefined,
+ tokenName: undefined,
+ }
+ : undefined,
+ surfaces,
+ };
+};
+
+export interface InfrastructureOperationsStateOptions {
+ embedded?: boolean;
+}
+
+export const useInfrastructureOperationsState = (
+ options: InfrastructureOperationsStateOptions = {},
+) => {
+ const { state } = useWebSocket();
+ const { resources, mutate: mutateResources, refetch: refetchResources } = useResources();
+ const navigate = useNavigate();
+
+ const pd = (r: Resource) => {
+ const platformData = getPlatformDataRecord(r);
+ return platformData ? unwrap(platformData) : undefined;
+ };
+ const asRecord = (value: unknown) =>
+ value && typeof value === 'object' ? (value as Record) : undefined;
+ const asBoolean = (value: unknown) => (typeof value === 'boolean' ? value : undefined);
+ const platformAgent = (r: Resource) => asRecord(getPlatformAgentRecord(r));
+
+ const getAgentId = (r: Resource) => getExplicitAgentIdFromResource(r);
+
+ const getAgentActionId = (r: Resource) => getActionableAgentIdFromResource(r);
+
+ const getDockerActionId = (r: Resource) => getActionableDockerRuntimeIdFromResource(r);
+
+ const getKubernetesActionId = (r: Resource) => getActionableKubernetesClusterIdFromResource(r);
+
+ const getCommandsEnabled = (r: Resource) => {
+ const platformData = pd(r);
+ return (
+ r.agent?.commandsEnabled ??
+ asBoolean(platformAgent(r)?.commandsEnabled) ??
+ asBoolean(platformData?.commandsEnabled)
+ );
+ };
+
+ let hasLoggedSecurityStatusError = false;
+
+ const [securityStatus, setSecurityStatus] = createSignal(null);
+ const [latestRecord, setLatestRecord] = createSignal(null);
+ const [tokenName, setTokenName] = createSignal('');
+ const [confirmedNoToken, setConfirmedNoToken] = createSignal(false);
+ const [currentToken, setCurrentToken] = createSignal(null);
+ const [isGeneratingToken, setIsGeneratingToken] = createSignal(false);
+ const [lookupValue, setLookupValue] = createSignal('');
+ const [lookupResult, setLookupResult] = createSignal(null);
+ const [lookupError, setLookupError] = createSignal(null);
+ const [lookupLoading, setLookupLoading] = createSignal(false);
+ const [insecureMode, setInsecureMode] = createSignal(false); // For self-signed certificates (issue #806)
+ const [enableCommands, setEnableCommands] = createSignal(false); // Enable Pulse command execution (issue #903)
+ const [installProfile, setInstallProfile] = createSignal('auto');
+ const [customAgentUrl, setCustomAgentUrl] = createSignal('');
+ const [customCaPath, setCustomCaPath] = createSignal('');
+ const [setupHandoff, setSetupHandoff] = createSignal(null);
+ const [profiles, setProfiles] = createSignal([]);
+ const [assignments, setAssignments] = createSignal([]);
+ // Track pending command config changes: agentId -> { desired value, timestamp }
+ const [pendingCommandConfig, setPendingCommandConfig] = createSignal<
+ Record
+ >({});
+ const [pendingScopeUpdates, setPendingScopeUpdates] = createSignal>({});
+ const [pendingInventoryActions, setPendingInventoryActions] = createSignal<
+ Record
+ >({});
+ const [inventoryActionNotice, setInventoryActionNotice] =
+ createSignal(null);
+ const [optimisticRemovedHostAgents, setOptimisticRemovedHostAgents] = createSignal<
+ RemovedHostAgent[]
+ >([]);
+ const [optimisticRemovedDockerHosts, setOptimisticRemovedDockerHosts] = createSignal<
+ RemovedDockerHost[]
+ >([]);
+ const [optimisticRemovedKubernetesClusters, setOptimisticRemovedKubernetesClusters] =
+ createSignal([]);
+ const [stopMonitoringDialog, setStopMonitoringDialog] =
+ createSignal(null);
+ const [expandedRowKey, setExpandedRowKey] = createSignal(null);
+ const [selectedIgnoredRowKey, setSelectedIgnoredRowKey] = createSignal(null);
+ const [filterCapability, setFilterCapability] = createSignal<'all' | AgentCapability>('all');
+ const [filterScope, setFilterScope] = createSignal<'all' | Exclude>('all');
+ const [filterSearch, setFilterSearch] = createSignal('');
+ let recoveryQueueSectionRef: HTMLDivElement | undefined;
+
+ const loadAgentProfiles = async () => {
+ try {
+ const [profilesData, assignmentsData] = await Promise.all([
+ AgentProfilesAPI.listProfiles(),
+ AgentProfilesAPI.listAssignments(),
+ ]);
+ setProfiles(profilesData);
+ setAssignments(assignmentsData);
+ } catch (err) {
+ logger.debug('Failed to load agent profiles', err);
+ notificationStore.error(err instanceof Error ? err.message : 'Failed to load agent profiles');
+ }
+ };
+
+ createEffect(() => {
+ if (requiresToken()) {
+ setConfirmedNoToken(false);
+ }
+ });
+
+ // Use agentUrl from API (PULSE_PUBLIC_URL) if configured, otherwise fall back to window.location
+ const agentUrl = () => securityStatus()?.agentUrl || getPulseBaseUrl();
+ const selectedAgentUrl = () => resolveInstallerBaseUrl(customAgentUrl(), agentUrl());
+
+ onMount(() => {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ const rawSetupHandoff = sessionStorage.getItem(STORAGE_KEYS.SETUP_HANDOFF);
+ if (rawSetupHandoff) {
+ try {
+ const parsed = JSON.parse(rawSetupHandoff) as Partial;
+ if (parsed.username && parsed.password && parsed.apiToken) {
+ setSetupHandoff({
+ username: parsed.username,
+ password: parsed.password,
+ apiToken: parsed.apiToken,
+ createdAt: parsed.createdAt,
+ });
+ } else {
+ sessionStorage.removeItem(STORAGE_KEYS.SETUP_HANDOFF);
+ }
+ } catch (_err) {
+ sessionStorage.removeItem(STORAGE_KEYS.SETUP_HANDOFF);
+ }
+ }
+
+ const fetchSecurityStatus = async () => {
+ try {
+ const data = await SecurityAPI.getStatus();
+ setSecurityStatus(data);
+ } catch (err) {
+ if (!hasLoggedSecurityStatusError) {
+ hasLoggedSecurityStatusError = true;
+ logger.error('Failed to load security status', err);
+ }
+ }
+ };
+ fetchSecurityStatus();
+ void loadAgentProfiles();
+ });
+
+ const clearSetupHandoff = () => {
+ if (typeof window !== 'undefined') {
+ try {
+ sessionStorage.removeItem(STORAGE_KEYS.SETUP_HANDOFF);
+ } catch (_err) {
+ // Ignore storage cleanup failures.
+ }
+ }
+ setSetupHandoff(null);
+ };
+
+ const copySetupHandoffField = async (value: string, successMessage: string) => {
+ const copied = await copyToClipboard(value);
+ if (copied) {
+ notificationStore.success(successMessage);
+ } else {
+ notificationStore.error(getUnifiedAgentClipboardCopyErrorMessage());
+ }
+ };
+
+ const downloadSetupHandoff = () => {
+ const handoff = setupHandoff();
+ if (!handoff) return;
+
+ const baseUrl = getPulseBaseUrl();
+ const content = `Pulse First-Run Credentials
+============================
+Generated: ${handoff.createdAt || new Date().toISOString()}
+
+Web Login:
+----------
+URL: ${baseUrl}
+Username: ${handoff.username}
+Password: ${handoff.password}
+
+Admin API Token:
+----------------
+${handoff.apiToken}
+
+Canonical install workspace:
+----------------------------
+${baseUrl.replace(/\/$/, '')}/settings/infrastructure/install
+
+Generate a scoped install token below before copying Unified Agent install commands.
+`;
+
+ const blob = new Blob([content], { type: 'text/plain' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = `pulse-first-run-credentials-${Date.now()}.txt`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ };
+
+ const requiresToken = () => {
+ const status = securityStatus();
+ if (status) {
+ return status.requiresAuth || status.apiTokenConfigured;
+ }
+ return true;
+ };
+
+ const hasToken = () => Boolean(currentToken());
+ const commandsUnlocked = () => (requiresToken() ? hasToken() : hasToken() || confirmedNoToken());
+
+ const acknowledgeNoToken = () => {
+ if (requiresToken()) {
+ notificationStore.info('Generate or select a token before continuing.', 4000);
+ return;
+ }
+ setCurrentToken(null);
+ setLatestRecord(null);
+ setConfirmedNoToken(true);
+ notificationStore.success('Confirmed install commands without an API token.', 3500);
+ };
+
+ const handleGenerateToken = async () => {
+ if (isGeneratingToken()) return;
+
+ setIsGeneratingToken(true);
+ try {
+ const desiredName = tokenName().trim() || buildDefaultTokenName();
+ // Generate token with unified agent reporting scopes
+ const scopes = [
+ AGENT_REPORT_SCOPE,
+ AGENT_CONFIG_READ_SCOPE,
+ DOCKER_REPORT_SCOPE,
+ KUBERNETES_REPORT_SCOPE,
+ AGENT_EXEC_SCOPE,
+ ];
+ const { token, record } = await SecurityAPI.createToken(desiredName, scopes);
+
+ setCurrentToken(token);
+ setLatestRecord(record);
+ setTokenName('');
+ setConfirmedNoToken(false);
+ trackAgentInstallTokenGenerated(UNIFIED_AGENT_TELEMETRY_SURFACE, 'manual');
+ notificationStore.success(
+ 'Token generated with Agent config + reporting, Docker, and Kubernetes permissions.',
+ 4000,
+ );
+ } catch (err) {
+ logger.error('Failed to generate agent token', err);
+ notificationStore.error(
+ 'Failed to generate agent token. Confirm you are signed in as an administrator.',
+ 6000,
+ );
+ } finally {
+ setIsGeneratingToken(false);
+ }
+ };
+
+ const resolvedCommandToken = () => {
+ if (requiresToken()) {
+ return currentToken() || TOKEN_PLACEHOLDER;
+ }
+ return currentToken();
+ };
+
+ const handleLookup = async () => {
+ const query = lookupValue().trim();
+ setLookupError(null);
+
+ if (!query) {
+ setLookupResult(null);
+ setLookupError('Enter a hostname or agent ID to check.');
+ return;
+ }
+
+ setLookupLoading(true);
+ try {
+ const result = await MonitoringAPI.lookupAgent({ id: query, hostname: query });
+ if (!result) {
+ setLookupResult(null);
+ setLookupError(`No agent has reported with "${query}" yet. Try again in a few seconds.`);
+ } else {
+ setLookupResult(result);
+ setLookupError(null);
+ }
+ } catch (err) {
+ const message = err instanceof Error ? err.message : 'Agent lookup failed.';
+ setLookupResult(null);
+ setLookupError(message);
+ } finally {
+ setLookupLoading(false);
+ }
+ };
+
+ const withPrivilegeEscalation = (command: string) => {
+ if (!command.includes('| bash -s --')) return command;
+ return command.replace(/\|\s*bash -s --([\s\S]*)$/, (_match, args: string) => {
+ return `| { if [ "$(id -u)" -eq 0 ]; then bash -s --${args}; elif command -v sudo >/dev/null 2>&1; then sudo bash -s --${args}; else echo "Root privileges required. Run as root (su -) and retry." >&2; exit 1; fi; }`;
+ });
+ };
+ const selectedCustomCaPath = () => customCaPath().trim();
+ const urlRequiresInstallerInsecure = (url: string) =>
+ insecureMode() || url.trim().toLowerCase().startsWith('http://');
+ const getInsecureFlag = (url: string) => (urlRequiresInstallerInsecure(url) ? ' --insecure' : '');
+ const getCurlFlags = () => (insecureMode() ? '-kfsSL' : '-fsSL');
+ const getShellCustomCaCurlFlag = () => {
+ const caPath = selectedCustomCaPath();
+ return caPath ? ` --cacert ${shellQuoteArg(caPath)}` : '';
+ };
+ const getShellCustomCaInstallerFlag = () => {
+ const caPath = selectedCustomCaPath();
+ return caPath ? ` --cacert ${shellQuoteArg(caPath)}` : '';
+ };
+ const getSelectedInstallProfile = () =>
+ INSTALL_PROFILE_OPTIONS.find((option) => option.value === installProfile()) ??
+ INSTALL_PROFILE_OPTIONS[0];
+ const getInstallProfileFlags = () => getSelectedInstallProfile().flags;
+ const getPowerShellInstallProfileEnvFromFlags = (flags: string[]) => {
+ const envAssignments: string[] = [];
+ for (let index = 0; index < flags.length; index += 1) {
+ const flag = flags[index];
+ switch (flag) {
+ case '--enable-docker':
+ envAssignments.push(`$env:PULSE_ENABLE_DOCKER="true"`);
+ break;
+ case '--disable-host':
+ envAssignments.push(`$env:PULSE_ENABLE_HOST="false"`);
+ break;
+ case '--enable-kubernetes':
+ envAssignments.push(`$env:PULSE_ENABLE_KUBERNETES="true"`);
+ break;
+ case '--enable-proxmox':
+ envAssignments.push(`$env:PULSE_ENABLE_PROXMOX="true"`);
+ break;
+ case '--proxmox-type':
+ if (typeof flags[index + 1] === 'string' && flags[index + 1].trim()) {
+ envAssignments.push(`$env:PULSE_PROXMOX_TYPE="${flags[index + 1].trim()}"`);
+ index += 1;
+ }
+ break;
+ default:
+ if (flag.startsWith('--proxmox-type ')) {
+ const proxmoxType = flag.slice('--proxmox-type '.length).trim();
+ if (proxmoxType) {
+ envAssignments.push(`$env:PULSE_PROXMOX_TYPE="${proxmoxType}"`);
+ }
+ }
+ break;
+ }
+ }
+ return envAssignments;
+ };
+ const getPowerShellInstallProfileEnv = () =>
+ getPowerShellInstallProfileEnvFromFlags(getInstallProfileFlags());
+ const getPowerShellTransportEnv = () => {
+ const envAssignments: string[] = [];
+ if (insecureMode()) {
+ envAssignments.push(`$env:PULSE_INSECURE_SKIP_VERIFY="true"`);
+ }
+ if (selectedCustomCaPath()) {
+ envAssignments.push(`$env:PULSE_CACERT="${powerShellQuote(selectedCustomCaPath())}"`);
+ }
+ return envAssignments;
+ };
+ const getPowerShellModeEnv = () => {
+ const envAssignments = getPowerShellTransportEnv();
+ if (enableCommands()) {
+ envAssignments.push(`$env:PULSE_ENABLE_COMMANDS="true"`);
+ }
+ return envAssignments;
+ };
+ const getPowerShellInstallEnv = () => {
+ const envAssignments = getPowerShellInstallProfileEnv();
+ if (enableCommands()) {
+ envAssignments.push(`$env:PULSE_ENABLE_COMMANDS="true"`);
+ }
+ return envAssignments;
+ };
+ const getInstallerExtraArgs = () => [
+ ...getInstallProfileFlags(),
+ ...(enableCommands() ? ['--enable-commands'] : []),
+ ];
+ const handleInstallProfileChange = (profile: InstallProfile) => {
+ setInstallProfile(profile);
+ trackAgentInstallProfileSelected(UNIFIED_AGENT_TELEMETRY_SURFACE, profile);
+ };
+
+ const getCanonicalUninstallAgentId = (row?: UnifiedAgentRow) =>
+ row?.agentActionId?.trim() || row?.agentId?.trim() || '';
+ const getCanonicalUninstallHostname = (row?: UnifiedAgentRow) => row?.hostname?.trim() || '';
+
+ const getUninstallCommand = (row?: UnifiedAgentRow) => {
+ const url = selectedAgentUrl();
+ const token = resolvedCommandToken();
+ const insecure = getInsecureFlag(url);
+ const agentId = getCanonicalUninstallAgentId(row);
+ const hostname = getCanonicalUninstallHostname(row);
+ const baseArgs = token
+ ? `--uninstall --url ${shellQuoteArg(url)} --token ${shellQuoteArg(token)}${insecure}${getShellCustomCaInstallerFlag()}`
+ : `--uninstall --url ${shellQuoteArg(url)}${insecure}${getShellCustomCaInstallerFlag()}`;
+ const identityArgs = `${agentId ? ` --agent-id ${shellQuoteArg(agentId)}` : ''}${hostname ? ` --hostname ${shellQuoteArg(hostname)}` : ''}`;
+ return withPrivilegeEscalation(
+ `curl ${getCurlFlags()}${getShellCustomCaCurlFlag()} ${shellQuoteArg(`${url}/install.sh`)} | bash -s -- ${baseArgs}${identityArgs}`,
+ );
+ };
+
+ const getWindowsUninstallCommand = (row?: UnifiedAgentRow) => {
+ const url = selectedAgentUrl();
+ const token = resolvedCommandToken();
+ const transportEnv = getPowerShellTransportEnv();
+ const agentId = getCanonicalUninstallAgentId(row);
+ const hostname = getCanonicalUninstallHostname(row);
+ const identityEnv: string[] = [];
+ if (agentId) {
+ identityEnv.push(`$env:PULSE_AGENT_ID="${powerShellQuote(agentId)}"`);
+ }
+ if (hostname) {
+ identityEnv.push(`$env:PULSE_HOSTNAME="${powerShellQuote(hostname)}"`);
+ }
+ const prefixParts = [...transportEnv, ...identityEnv];
+ const prefix = prefixParts.length > 0 ? `${prefixParts.join('; ')}; ` : '';
+ // Include URL and token for server notification (removes agent from dashboard)
+ if (token) {
+ return `${prefix}$env:PULSE_URL="${powerShellQuote(url)}"; $env:PULSE_TOKEN="${powerShellQuote(token)}"; $env:PULSE_UNINSTALL="true"; ${buildPowerShellInstallScriptBootstrap(url)}`;
+ }
+ return `${prefix}$env:PULSE_URL="${powerShellQuote(url)}"; $env:PULSE_UNINSTALL="true"; ${buildPowerShellInstallScriptBootstrap(url)}`;
+ };
+
+ const getPlatformUninstallCommand = (platform: AgentPlatform, row?: UnifiedAgentRow) => {
+ if (platform === 'windows') {
+ return getWindowsUninstallCommand(row);
+ }
+ return getUninstallCommand(row);
+ };
+
+ const commandSections = createMemo(() => {
+ const url = selectedAgentUrl();
+ const token = resolvedCommandToken();
+ const unixCommand = buildUnixAgentInstallCommand({
+ baseUrl: url,
+ token,
+ insecure: insecureMode(),
+ caCertPath: selectedCustomCaPath(),
+ extraArgs: getInstallerExtraArgs(),
+ });
+ const windowsInteractiveCommand = buildWindowsAgentInstallCommand({
+ baseUrl: url,
+ token: currentToken(),
+ insecure: insecureMode(),
+ caCertPath: selectedCustomCaPath(),
+ extraEnvAssignments: getPowerShellInstallEnv(),
+ });
+ const windowsParameterizedCommand = buildWindowsAgentInstallCommand({
+ baseUrl: url,
+ token,
+ insecure: insecureMode(),
+ caCertPath: selectedCustomCaPath(),
+ extraEnvAssignments: getPowerShellInstallEnv(),
+ });
+ const commands = buildCommandsByPlatform(
+ unixCommand,
+ windowsInteractiveCommand,
+ windowsParameterizedCommand,
+ );
+ return Object.entries(commands).map(([platform, meta]) => ({
+ platform: platform as AgentPlatform,
+ ...meta,
+ }));
+ });
+
+ const matchesRemovedAgent = (
+ resource: Resource,
+ ids: { agentId?: string; dockerId?: string },
+ capabilities: AgentCapability[],
+ ) => {
+ if (capabilities.includes('agent') && ids.agentId) {
+ const agentId = ids.agentId;
+ if (
+ resource.id === agentId ||
+ getAgentId(resource) === agentId ||
+ getAgentActionId(resource) === agentId
+ ) {
+ return true;
+ }
+ }
+
+ if (capabilities.includes('docker') && ids.dockerId) {
+ const dockerId = ids.dockerId;
+ if (resource.id === dockerId || getDockerActionId(resource) === dockerId) {
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ const reconcileRemovedAgent = (
+ ids: { agentId?: string; dockerId?: string },
+ capabilities: AgentCapability[],
+ row: UnifiedAgentRow,
+ ) => {
+ const removedAt = Date.now();
+ if (capabilities.includes('agent') && ids.agentId) {
+ setOptimisticRemovedHostAgents((prev) => [
+ {
+ id: ids.agentId!,
+ hostname: row.hostname,
+ displayName: row.displayName || row.name,
+ removedAt,
+ },
+ ...prev.filter((item) => item.id !== ids.agentId),
+ ]);
+ }
+ if (capabilities.includes('docker') && ids.dockerId) {
+ setOptimisticRemovedDockerHosts((prev) => [
+ {
+ id: ids.dockerId!,
+ hostname: row.hostname,
+ displayName: row.displayName || row.name,
+ removedAt,
+ },
+ ...prev.filter((item) => item.id !== ids.dockerId),
+ ]);
+ }
+ mutateResources((prev) =>
+ prev.filter((resource) => !matchesRemovedAgent(resource, ids, capabilities)),
+ );
+ void refetchResources().catch((err) => {
+ logger.debug('Failed to refresh resources after agent removal', err);
+ });
+ };
+
+ const reconcileRemovedKubernetesCluster = (clusterId: string, clusterName?: string) => {
+ setOptimisticRemovedKubernetesClusters((prev) => [
+ {
+ id: clusterId,
+ name: clusterName || clusterId,
+ displayName: clusterName,
+ removedAt: Date.now(),
+ },
+ ...prev.filter((item) => item.id !== clusterId),
+ ]);
+ mutateResources((prev) =>
+ prev.filter(
+ (resource) =>
+ !(
+ resource.type === 'k8s-cluster' &&
+ (getKubernetesActionId(resource) === clusterId || resource.id === clusterId)
+ ),
+ ),
+ );
+ void refetchResources().catch((err) => {
+ logger.debug('Failed to refresh resources after kubernetes removal', err);
+ });
+ };
+
+ /**
+ * All resources managed by an agent.
+ * In v6, the backend already merges resources by identity — a PVE node with a
+ * linked agent is a single resource of type "agent" with agent + proxmox data.
+ * No frontend merge logic or type-flapping prevention needed.
+ *
+ * Includes docker-host resources that may lack the `agent` facet when the
+ * agent's docker data is represented as a separate resource type.
+ */
+ const agentResources = createMemo(() => {
+ return resources()
+ .filter((r) => r.type !== 'k8s-cluster' && (hasAgentFacet(r) || hasDockerWorkloadsScope(r)))
+ .sort((a, b) =>
+ (getPreferredResourceHostname(a) || '').localeCompare(
+ getPreferredResourceHostname(b) || '',
+ ),
+ );
+ });
+
+ const agentByActionId = createMemo(() => {
+ const map = new Map();
+ // Only include resources with an agent facet (not docker-only resources)
+ // to avoid polluting the command config sync lookup.
+ for (const agentResource of agentResources()) {
+ if (!hasAgentFacet(agentResource)) continue;
+ const actionId = getAgentActionId(agentResource);
+ if (!actionId || map.has(actionId)) continue;
+ map.set(actionId, agentResource);
+ }
+ return map;
+ });
+
+ const profileById = createMemo(() => {
+ const map = new Map();
+ for (const profile of profiles()) {
+ map.set(profile.id, profile);
+ }
+ return map;
+ });
+
+ const assignmentByAgent = createMemo(() => {
+ const map = new Map();
+ for (const assignment of assignments()) {
+ map.set(assignment.agent_id, assignment);
+ }
+ return map;
+ });
+
+ const getScopeInfo = (agentId: string | undefined) => {
+ if (!agentId) {
+ return { label: 'N/A', detail: '', category: 'na' as const };
+ }
+ const assignment = assignmentByAgent().get(agentId);
+ if (!assignment) {
+ return { label: 'Default', detail: 'Auto-detect', category: 'default' as const };
+ }
+ const profile = profileById().get(assignment.profile_id);
+ if (!profile) {
+ return {
+ label: 'Profile assigned',
+ detail: assignment.profile_id,
+ category: 'profile' as const,
+ };
+ }
+ const name = profile.name || assignment.profile_id;
+ const isAIManaged =
+ profile.description?.toLowerCase().includes('pulse ai') ||
+ name.toLowerCase().startsWith('ai scope');
+ return isAIManaged
+ ? { label: 'Patrol-managed', detail: name, category: 'ai-managed' as const }
+ : { label: name, detail: 'Assigned profile', category: 'profile' as const };
+ };
+
+ const getProfileOptionLabel = (profileId: string) => {
+ const profile = profileById().get(profileId);
+ if (profile) {
+ return profile.name || profile.id;
+ }
+ return `Missing profile (${profileId})`;
+ };
+
+ const updateScopeAssignment = async (
+ agentId: string,
+ profileId: string | null,
+ agentName: string,
+ ) => {
+ if (!agentId) {
+ return;
+ }
+ if (pendingScopeUpdates()[agentId]) {
+ return;
+ }
+
+ setPendingScopeUpdates((prev) => ({ ...prev, [agentId]: true }));
+ try {
+ if (profileId) {
+ await AgentProfilesAPI.assignProfile(agentId, profileId);
+ setAssignments((prev) => {
+ const updatedAt = new Date().toISOString();
+ const next = prev.filter((a) => a.agent_id !== agentId);
+ next.push({ agent_id: agentId, profile_id: profileId, updated_at: updatedAt });
+ return next;
+ });
+ notificationStore.success(
+ `Scope updated for ${agentName}. Restart the agent to apply changes.`,
+ );
+ } else {
+ await AgentProfilesAPI.unassignProfile(agentId);
+ setAssignments((prev) => prev.filter((a) => a.agent_id !== agentId));
+ notificationStore.success(
+ `Scope reset for ${agentName}. Restart the agent to apply changes.`,
+ );
+ }
+ } catch (err) {
+ logger.error('Failed to update agent scope', err);
+ if (err instanceof Error && err.message === MISSING_AGENT_PROFILE_ASSIGNMENT_MESSAGE) {
+ await loadAgentProfiles();
+ }
+ notificationStore.error(
+ err instanceof Error && err.message ? err.message : 'Failed to update agent scope',
+ );
+ } finally {
+ setPendingScopeUpdates((prev) => {
+ const next = { ...prev };
+ delete next[agentId];
+ return next;
+ });
+ }
+ };
+
+ const handleResetScope = async (agentId: string, agentName: string) => {
+ if (
+ !confirm(
+ `Reset scope for ${agentName}? This removes any assigned profile and reverts to auto-detect.`,
+ )
+ )
+ return;
+ await updateScopeAssignment(agentId, null, agentName);
+ };
+
+ const toggleAgentDetails = (rowKey: string) => {
+ setExpandedRowKey(rowKey);
+ };
+
+ const connectedInfrastructureItems = createMemo(
+ () => state.connectedInfrastructure,
+ );
+ const isEmbedded = () => options.embedded ?? false;
+
+ const unifiedRows = createMemo(() => {
+ const rows: UnifiedAgentRow[] = [];
+ const optimisticHostIDs = new Set(
+ optimisticRemovedHostAgents()
+ .map((item) => item.id?.trim())
+ .filter((id): id is string => Boolean(id)),
+ );
+ const optimisticDockerIDs = new Set(
+ optimisticRemovedDockerHosts()
+ .map((item) => item.id?.trim())
+ .filter((id): id is string => Boolean(id)),
+ );
+ const optimisticKubernetesIDs = new Set(
+ optimisticRemovedKubernetesClusters()
+ .map((item) => item.id?.trim())
+ .filter((id): id is string => Boolean(id)),
+ );
+
+ connectedInfrastructureItems().forEach((item) => {
+ const optimisticFilteredItem: ConnectedInfrastructureItem =
+ item.status === 'active'
+ ? {
+ ...item,
+ surfaces: item.surfaces.filter((surface) => {
+ const controlId = surface.controlId?.trim();
+ if (!controlId) return true;
+ if (surface.kind === 'agent') return !optimisticHostIDs.has(controlId);
+ if (surface.kind === 'docker') return !optimisticDockerIDs.has(controlId);
+ if (surface.kind === 'kubernetes') return !optimisticKubernetesIDs.has(controlId);
+ return true;
+ }),
+ }
+ : item;
+
+ if (optimisticFilteredItem.status === 'active' && optimisticFilteredItem.surfaces.length === 0) {
+ return;
+ }
+
+ rows.push(
+ rowFromConnectedInfrastructureItem(
+ optimisticFilteredItem,
+ getScopeInfo(optimisticFilteredItem.scopeAgentId),
+ ),
+ );
+ });
+
+ const backendIgnoredSurfaceKeys = new Set(
+ rows
+ .filter((row) => row.status === 'removed')
+ .flatMap((row) =>
+ row.surfaces.map((surface) => `${surface.kind}:${surface.controlId || surface.idValue || row.id}`),
+ ),
+ );
+
+ optimisticRemovedDockerHosts().forEach((runtime) => {
+ const key = `docker:${runtime.id}`;
+ if (backendIgnoredSurfaceKeys.has(key)) return;
+ const name = getPreferredNamedEntityLabel(runtime);
+ rows.push({
+ rowKey: `removed-docker-${runtime.id}`,
+ id: runtime.id,
+ dockerActionId: runtime.id,
+ name,
+ hostname: runtime.hostname,
+ displayName: runtime.displayName,
+ capabilities: ['docker'],
+ status: 'removed',
+ removedAt: runtime.removedAt,
+ upgradePlatform: 'linux',
+ scope: getScopeInfo(undefined),
+ installFlags: ['--enable-docker', '--disable-host'],
+ searchText: [name, runtime.hostname, runtime.id].filter(Boolean).join(' ').toLowerCase(),
+ surfaces: [
+ {
+ key: 'docker',
+ kind: 'docker',
+ label: 'Docker runtime data',
+ detail: 'Pulse is blocking Docker runtime reports from this machine.',
+ idLabel: 'Docker runtime ID',
+ idValue: runtime.id,
+ action: 'allow-reconnect',
+ controlId: runtime.id,
+ },
+ ],
+ });
+ });
+
+ optimisticRemovedHostAgents().forEach((host) => {
+ const key = `agent:${host.id}`;
+ if (backendIgnoredSurfaceKeys.has(key)) return;
+ const name = getPreferredNamedEntityLabel(host);
+ rows.push({
+ rowKey: `removed-host-${host.id}`,
+ id: host.id,
+ agentActionId: host.id,
+ name,
+ hostname: host.hostname,
+ displayName: host.displayName,
+ capabilities: ['agent'],
+ status: 'removed',
+ removedAt: host.removedAt,
+ upgradePlatform: 'linux',
+ scope: getScopeInfo(undefined),
+ installFlags: [],
+ searchText: [name, host.hostname, host.id].filter(Boolean).join(' ').toLowerCase(),
+ surfaces: [
+ {
+ key: 'agent',
+ kind: 'agent',
+ label: 'Host telemetry',
+ detail: 'Pulse is blocking host telemetry from this machine.',
+ idLabel: 'Agent ID',
+ idValue: host.id,
+ action: 'allow-reconnect',
+ controlId: host.id,
+ },
+ ],
+ });
+ });
+
+ optimisticRemovedKubernetesClusters().forEach((cluster) => {
+ const key = `kubernetes:${cluster.id}`;
+ if (backendIgnoredSurfaceKeys.has(key)) return;
+ const name = getPreferredNamedEntityLabel(cluster);
+ rows.push({
+ rowKey: `removed-k8s-${cluster.id}`,
+ id: cluster.id,
+ kubernetesActionId: cluster.id,
+ name,
+ capabilities: ['kubernetes'],
+ status: 'removed',
+ removedAt: cluster.removedAt,
+ upgradePlatform: 'linux',
+ scope: getScopeInfo(undefined),
+ installFlags: ['--enable-kubernetes'],
+ searchText: [name, cluster.name, cluster.id].filter(Boolean).join(' ').toLowerCase(),
+ surfaces: [
+ {
+ key: 'kubernetes',
+ kind: 'kubernetes',
+ label: 'Kubernetes cluster data',
+ detail: 'Pulse is blocking Kubernetes telemetry for this cluster.',
+ idLabel: 'Cluster ID',
+ idValue: cluster.id,
+ action: 'allow-reconnect',
+ controlId: cluster.id,
+ },
+ ],
+ });
+ });
+
+ rows.sort((a, b) => {
+ if (a.status !== b.status) {
+ return a.status === 'active' ? -1 : 1;
+ }
+ return a.name.localeCompare(b.name);
+ });
+
+ return rows;
+ });
+
+ const matchesInventoryFilters = (row: UnifiedAgentRow) => {
+ const query = filterSearch().trim().toLowerCase();
+ if (
+ filterCapability() !== 'all' &&
+ !row.capabilities.includes(filterCapability() as AgentCapability)
+ ) {
+ return false;
+ }
+ if (filterScope() !== 'all' && row.scope.category !== filterScope()) {
+ return false;
+ }
+ if (query && !row.searchText.includes(query)) {
+ return false;
+ }
+ return true;
+ };
+
+ const activeRows = createMemo(() => unifiedRows().filter((row) => row.status === 'active'));
+ const monitoringStoppedRows = createMemo(() =>
+ unifiedRows().filter((row) => row.status === 'removed'),
+ );
+ const linkedAgents = createMemo(() =>
+ activeRows()
+ .filter((row) => Boolean(row.linkedNodeId))
+ .map((row) => ({
+ id: row.id,
+ hostname: row.hostname || row.name,
+ displayName: row.displayName,
+ linkedNodeId: row.linkedNodeId!,
+ status: row.healthStatus || 'online',
+ version: row.version,
+ lastSeen: row.lastSeen,
+ })),
+ );
+ const hasLinkedAgents = createMemo(() => linkedAgents().length > 0);
+ const outdatedAgents = createMemo(() => activeRows().filter((row) => row.isOutdatedBinary));
+ const hasOutdatedAgents = createMemo(() => outdatedAgents().length > 0);
+
+ const filteredActiveRows = createMemo(() => activeRows().filter(matchesInventoryFilters));
+ const filteredMonitoringStoppedRows = createMemo(() =>
+ monitoringStoppedRows().filter(matchesInventoryFilters),
+ );
+ const selectedActiveRow = createMemo(() => {
+ const selectedKey = expandedRowKey();
+ return filteredActiveRows().find((row) => row.rowKey === selectedKey) || null;
+ });
+ const selectedIgnoredRow = createMemo(() => {
+ const selectedKey = selectedIgnoredRowKey();
+ return filteredMonitoringStoppedRows().find((row) => row.rowKey === selectedKey) || null;
+ });
+
+ const hasFilters = createMemo(() => {
+ return (
+ filterCapability() !== 'all' || filterScope() !== 'all' || filterSearch().trim().length > 0
+ );
+ });
+
+ const hasMonitoringStoppedRows = createMemo(() => monitoringStoppedRows().length > 0);
+ const showMonitoringStoppedSection = createMemo(() => {
+ return hasMonitoringStoppedRows() || hasFilters();
+ });
+ const coverageSummary = createMemo(() => {
+ const active = activeRows();
+ return {
+ agent: active.filter((row) => row.capabilities.includes('agent')).length,
+ docker: active.filter((row) => row.capabilities.includes('docker')).length,
+ kubernetes: active.filter((row) => row.capabilities.includes('kubernetes')).length,
+ proxmox: active.filter((row) => row.capabilities.includes('proxmox')).length,
+ pbs: active.filter((row) => row.capabilities.includes('pbs')).length,
+ pmg: active.filter((row) => row.capabilities.includes('pmg')).length,
+ };
+ });
+
+ const reportingCoverageSummaryText = createMemo(() => {
+ const summary = coverageSummary();
+ const activeClauses = [
+ summary.agent > 0 ? `${summary.agent} host${summary.agent === 1 ? '' : 's'}` : null,
+ summary.docker > 0
+ ? `${summary.docker} Docker runtime${summary.docker === 1 ? '' : 's'}`
+ : null,
+ summary.kubernetes > 0
+ ? `${summary.kubernetes} Kubernetes cluster${summary.kubernetes === 1 ? '' : 's'}`
+ : null,
+ summary.proxmox > 0
+ ? `${summary.proxmox} Proxmox node${summary.proxmox === 1 ? '' : 's'}`
+ : null,
+ summary.pbs > 0 ? `${summary.pbs} PBS server${summary.pbs === 1 ? '' : 's'}` : null,
+ summary.pmg > 0 ? `${summary.pmg} PMG server${summary.pmg === 1 ? '' : 's'}` : null,
+ ].filter((value): value is string => Boolean(value));
+
+ if (activeClauses.length === 0) {
+ return 'Pulse is not currently receiving live infrastructure reports.';
+ }
+
+ return `Pulse is currently receiving live reports from ${joinHumanList(activeClauses)}.`;
+ });
+
+ const inventoryStatusSummaryText = createMemo(() => {
+ const activeCount = filteredActiveRows().length;
+ const recoveryCount = filteredMonitoringStoppedRows().length;
+ const recoveryClause =
+ recoveryCount > 0
+ ? `${recoveryCount} item${recoveryCount === 1 ? ' is' : 's are'} currently ignored by Pulse`
+ : 'nothing is currently ignored by Pulse';
+ return `${activeCount} item${activeCount === 1 ? ' is' : 's are'} actively reporting right now, and ${recoveryClause}. Stopping monitoring in Pulse does not uninstall software on the remote system.`;
+ });
+
+ const resetFilters = () => {
+ setFilterCapability('all');
+ setFilterScope('all');
+ setFilterSearch('');
+ };
+
+ const setInventoryActionPending = (
+ rowKey: string,
+ action: InventoryActionType,
+ pending: boolean,
+ ) => {
+ setPendingInventoryActions((prev) => {
+ const next = { ...prev };
+ if (pending) {
+ next[rowKey] = action;
+ } else {
+ delete next[rowKey];
+ }
+ return next;
+ });
+ };
+
+ const getPendingInventoryAction = (rowKey: string) => pendingInventoryActions()[rowKey];
+
+ const scrollToRecoveryQueue = () => {
+ recoveryQueueSectionRef?.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ };
+
+ const openStopMonitoringDialog = (row: UnifiedAgentRow) => {
+ setStopMonitoringDialog({
+ row,
+ subject: getInventorySubjectLabel(row.name, 'this item'),
+ scopeLabel: getStopMonitoringScopeLabel(row),
+ });
+ };
+
+ const getUpgradeCommand = (row: UnifiedAgentRow) => {
+ const token = resolvedCommandToken();
+ const url = selectedAgentUrl();
+ const agentId = getCanonicalUninstallAgentId(row);
+ const hostname = getCanonicalUninstallHostname(row);
+ if (row.upgradePlatform === 'windows') {
+ const envAssignments = [
+ ...getPowerShellInstallProfileEnvFromFlags(row.installFlags),
+ ...getPowerShellModeEnv(),
+ ];
+ if (agentId) {
+ envAssignments.push(`$env:PULSE_AGENT_ID="${powerShellQuote(agentId)}"`);
+ }
+ if (hostname) {
+ envAssignments.push(`$env:PULSE_HOSTNAME="${powerShellQuote(hostname)}"`);
+ }
+ const prefix = envAssignments.length > 0 ? `${envAssignments.join('; ')}; ` : '';
+ const tokenEnv = token ? `$env:PULSE_TOKEN="${powerShellQuote(token)}"; ` : '';
+ return `${prefix}$env:PULSE_URL="${powerShellQuote(url)}"; ${tokenEnv}${buildPowerShellInstallScriptBootstrap(url)}`;
+ }
+ let command = `curl ${getCurlFlags()}${getShellCustomCaCurlFlag()} ${shellQuoteArg(`${url}/install.sh`)} | bash -s -- --url ${shellQuoteArg(url)}`;
+ if (token) {
+ command += ` --token ${shellQuoteArg(token)}`;
+ }
+ if (row.installFlags.length > 0) {
+ command += ` ${row.installFlags.join(' ')}`;
+ }
+ if (urlRequiresInstallerInsecure(url)) {
+ command += getInsecureFlag(url);
+ }
+ command += getShellCustomCaInstallerFlag();
+ if (agentId) {
+ command += ` --agent-id ${shellQuoteArg(agentId)}`;
+ }
+ if (hostname) {
+ command += ` --hostname ${shellQuoteArg(hostname)}`;
+ }
+ return withPrivilegeEscalation(command);
+ };
+
+ const handleRemoveAgent = async (row: UnifiedAgentRow) => {
+ const subject = getInventorySubjectLabel(row.name, 'this host');
+
+ setInventoryActionNotice(null);
+ setInventoryActionPending(row.rowKey, 'stop-monitoring', true);
+ try {
+ let removed = false;
+ // Remove the agent registration
+ if (row.capabilities.includes('agent') && row.agentActionId) {
+ await MonitoringAPI.deleteAgent(row.agentActionId);
+ removed = true;
+ }
+ // Remove docker runtime registration if present
+ if (row.capabilities.includes('docker') && row.dockerActionId) {
+ await MonitoringAPI.deleteDockerRuntime(row.dockerActionId, { force: true });
+ removed = true;
+ }
+ if (removed) {
+ reconcileRemovedAgent(
+ { agentId: row.agentActionId, dockerId: row.dockerActionId },
+ row.capabilities,
+ row,
+ );
+ setInventoryActionNotice({
+ tone: 'success',
+ title: `Monitoring stopped for ${subject}`,
+ detail:
+ 'Pulse removed it from active reporting and will ignore new reports until you allow reconnect. You can review it in Ignored by Pulse below.',
+ showRecoveryQueueLink: true,
+ });
+ notificationStore.success(getUnifiedAgentStopMonitoringSuccessMessage(subject));
+ } else {
+ notificationStore.error(getUnifiedAgentStopMonitoringUnavailableMessage());
+ }
+ } catch (err) {
+ logger.error('Failed to stop monitoring host', err);
+ notificationStore.error(getUnifiedAgentStopMonitoringErrorMessage(subject));
+ } finally {
+ setInventoryActionPending(row.rowKey, 'stop-monitoring', false);
+ setStopMonitoringDialog(null);
+ }
+ };
+
+ const handleAllowHostReconnect = async (row: UnifiedAgentRow) => {
+ const agentId = row.agentActionId || row.id;
+ const subject = getInventorySubjectLabel(row.displayName || row.hostname || row.name, agentId);
+ setInventoryActionNotice(null);
+ setInventoryActionPending(row.rowKey, 'allow-reconnect', true);
+ try {
+ await MonitoringAPI.allowHostAgentReenroll(agentId);
+ setOptimisticRemovedHostAgents((prev) => prev.filter((item) => item.id !== agentId));
+ setInventoryActionNotice({
+ tone: 'info',
+ title: `Reconnect allowed for ${subject}`,
+ detail: 'Pulse will accept reports from it again the next time it checks in.',
+ });
+ notificationStore.success(getUnifiedAgentAllowReconnectSuccessMessage(subject));
+ } catch (err) {
+ logger.error('Failed to allow reconnect for host agent', err);
+ notificationStore.error(getUnifiedAgentAllowReconnectErrorMessage(subject));
+ } finally {
+ setInventoryActionPending(row.rowKey, 'allow-reconnect', false);
+ }
+ };
+
+ const handleAllowDockerReconnect = async (row: UnifiedAgentRow) => {
+ const agentId = row.dockerActionId || row.id;
+ const subject = getInventorySubjectLabel(row.displayName || row.hostname || row.name, agentId);
+ setInventoryActionNotice(null);
+ setInventoryActionPending(row.rowKey, 'allow-reconnect', true);
+ try {
+ await MonitoringAPI.allowDockerRuntimeReenroll(agentId);
+ setOptimisticRemovedDockerHosts((prev) => prev.filter((item) => item.id !== agentId));
+ setInventoryActionNotice({
+ tone: 'info',
+ title: `Reconnect allowed for ${subject}`,
+ detail: 'Pulse will accept reports from it again the next time it checks in.',
+ });
+ notificationStore.success(getUnifiedAgentAllowReconnectSuccessMessage(subject));
+ } catch (err) {
+ logger.error('Failed to allow reconnect for docker runtime', err);
+ notificationStore.error(getUnifiedAgentAllowReconnectErrorMessage(subject));
+ } finally {
+ setInventoryActionPending(row.rowKey, 'allow-reconnect', false);
+ }
+ };
+
+ const handleRemoveKubernetesCluster = async (row: UnifiedAgentRow) => {
+ const clusterId = row.kubernetesActionId || row.id;
+ const subject = getInventorySubjectLabel(row.name, 'this cluster');
+
+ setInventoryActionNotice(null);
+ setInventoryActionPending(row.rowKey, 'stop-monitoring', true);
+ try {
+ await MonitoringAPI.deleteKubernetesCluster(clusterId);
+ reconcileRemovedKubernetesCluster(clusterId, row.name);
+ setInventoryActionNotice({
+ tone: 'success',
+ title: `Monitoring stopped for ${subject}`,
+ detail:
+ 'Pulse removed it from active reporting and will ignore new reports until you allow reconnect. You can review it in Ignored by Pulse below.',
+ showRecoveryQueueLink: true,
+ });
+ notificationStore.success(getUnifiedAgentStopMonitoringSuccessMessage(subject));
+ } catch (err) {
+ logger.error('Failed to stop monitoring kubernetes cluster', err);
+ notificationStore.error(getUnifiedAgentStopMonitoringErrorMessage(subject));
+ } finally {
+ setInventoryActionPending(row.rowKey, 'stop-monitoring', false);
+ setStopMonitoringDialog(null);
+ }
+ };
+
+ const handleAllowKubernetesReconnect = async (row: UnifiedAgentRow) => {
+ const clusterId = row.kubernetesActionId || row.id;
+ const subject = getInventorySubjectLabel(row.name, clusterId);
+ setInventoryActionNotice(null);
+ setInventoryActionPending(row.rowKey, 'allow-reconnect', true);
+ try {
+ await MonitoringAPI.allowKubernetesClusterReenroll(clusterId);
+ setOptimisticRemovedKubernetesClusters((prev) =>
+ prev.filter((item) => item.id !== clusterId),
+ );
+ setInventoryActionNotice({
+ tone: 'info',
+ title: `Reconnect allowed for ${subject}`,
+ detail: 'Pulse will accept reports from it again the next time it checks in.',
+ });
+ notificationStore.success(getUnifiedAgentAllowReconnectSuccessMessage(subject));
+ } catch (err) {
+ logger.error('Failed to allow reconnect for kubernetes cluster', err);
+ notificationStore.error(getUnifiedAgentAllowReconnectErrorMessage(subject));
+ } finally {
+ setInventoryActionPending(row.rowKey, 'allow-reconnect', false);
+ }
+ };
+
+ // Clear pending state when agent reports matching the expected value, or after timeout
+ createEffect(() => {
+ const pending = pendingCommandConfig();
+ const agents = agentByActionId();
+ const now = Date.now();
+ const TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
+
+ // Check if any pending config now matches the reported state or has timed out
+ let updated = false;
+ const newPending = { ...pending };
+ const timedOut: string[] = [];
+
+ for (const agentId of Object.keys(pending)) {
+ const entry = pending[agentId];
+ const agent = agents.get(agentId);
+
+ const agentCommandsEnabled = agent ? getCommandsEnabled(agent) : undefined;
+ if (
+ agent &&
+ typeof agentCommandsEnabled === 'boolean' &&
+ agentCommandsEnabled === entry.enabled
+ ) {
+ // Agent confirmed the change
+ delete newPending[agentId];
+ updated = true;
+ } else if (now - entry.timestamp > TIMEOUT_MS) {
+ // Timed out waiting for agent
+ delete newPending[agentId];
+ const agentLabel = agent ? agent.identity?.hostname || agent.name || agentId : agentId;
+ timedOut.push(agentLabel);
+ updated = true;
+ }
+ }
+
+ if (updated) {
+ setPendingCommandConfig(newPending);
+ if (timedOut.length > 0) {
+ notificationStore.warning(
+ `Config sync timed out for ${timedOut.join(', ')}. Agent may be offline.`,
+ );
+ }
+ }
+ });
+
+ createEffect(() => {
+ const rows = filteredActiveRows();
+ const selectedKey = expandedRowKey();
+
+ if (rows.length === 0) {
+ if (selectedKey !== null) {
+ setExpandedRowKey(null);
+ }
+ return;
+ }
+
+ if (selectedKey && !rows.some((row) => row.rowKey === selectedKey)) {
+ setExpandedRowKey(null);
+ }
+ });
+
+ createEffect(() => {
+ const rows = filteredMonitoringStoppedRows();
+ const selectedKey = selectedIgnoredRowKey();
+
+ if (rows.length === 0) {
+ if (selectedKey !== null) {
+ setSelectedIgnoredRowKey(null);
+ }
+ return;
+ }
+
+ if (selectedKey && !rows.some((row) => row.rowKey === selectedKey)) {
+ setSelectedIgnoredRowKey(null);
+ }
+ });
+
+ const reportingColumns = [
+ {
+ key: 'name',
+ label: 'Name',
+ render: (row: UnifiedAgentRow) => {
+ const selected = () => expandedRowKey() === row.rowKey;
+ const agentName = row.displayName || row.hostname || row.name;
+ const reportingSummary = getRowReportingSummary(row);
+ return (
+
+
+
{row.name}
+
+ {row.hostname}
+
+
+ {reportingSummary}
+
+
+
{
+ e.stopPropagation();
+ toggleAgentDetails(row.rowKey);
+ }}
+ class={`inline-flex min-h-10 items-center justify-center rounded-md px-2.5 py-1.5 text-xs font-medium sm:min-h-9 ${
+ selected()
+ ? 'bg-blue-600 text-white shadow-sm hover:bg-blue-700'
+ : 'text-muted hover:bg-surface hover:text-base-content'
+ }`}
+ aria-label={`View details for ${agentName}`}
+ aria-pressed={selected()}
+ aria-controls={`agent-details-${row.rowKey}`}
+ >
+ {selected() ? 'Open details' : 'View details'}
+
+
+ );
+ },
+ },
+ {
+ key: 'capabilities',
+ label: 'Reporting surfaces',
+ render: (row: UnifiedAgentRow) => (
+
+
+ {(cap) => (
+
+
+ {getCapabilitySurfaceLabel(cap)}
+
+
+ )}
+
+
+ ),
+ },
+ {
+ key: 'status',
+ label: 'Status',
+ render: (row: UnifiedAgentRow) => {
+ const statusPresentation = () =>
+ getUnifiedAgentStatusPresentation(row.status, row.healthStatus);
+ return (
+
+ {statusPresentation().label}
+
+ );
+ },
+ },
+ {
+ key: 'lastSeen',
+ label: 'Last Seen',
+ render: (row: UnifiedAgentRow) => (
+
+ {getUnifiedAgentLastSeenLabel(row, MONITORING_STOPPED_STATUS_LABEL)}
+
+ ),
+ },
+ {
+ key: 'actions',
+ label: 'Actions',
+ align: 'right' as const,
+ render: (row: UnifiedAgentRow) => {
+ const isRemoved = () => row.status === 'removed';
+ const isKubernetes = () =>
+ row.capabilities.includes('kubernetes') && !row.capabilities.includes('agent');
+ const pendingAction = () => getPendingInventoryAction(row.rowKey);
+ const isStopping = () => pendingAction() === 'stop-monitoring';
+ const isAllowingReconnect = () => pendingAction() === 'allow-reconnect';
+ const canRemove = () => {
+ const needsAgent = row.capabilities.includes('agent') && !row.agentActionId;
+ const needsDocker =
+ row.capabilities.includes('docker') && !row.dockerActionId && !row.agentActionId;
+ return !needsAgent && !needsDocker;
+ };
+ return (
+ openStopMonitoringDialog(row)}
+ disabled={!canRemove() || Boolean(pendingAction())}
+ title={!canRemove() ? 'Agent ID unavailable for removal' : undefined}
+ class="inline-flex min-h-10 sm:min-h-9 items-center rounded-md px-2.5 py-1.5 text-sm font-medium text-red-600 hover:bg-red-50 hover:text-red-900 disabled:cursor-not-allowed disabled:opacity-50 dark:text-red-400 dark:hover:bg-red-900 dark:hover:text-red-300"
+ >
+ {isStopping() ? 'Stopping…' : 'Stop monitoring'}
+
+ }
+ >
+ openStopMonitoringDialog(row)}
+ disabled={Boolean(pendingAction())}
+ class="inline-flex min-h-10 sm:min-h-9 items-center rounded-md px-2.5 py-1.5 text-sm font-medium text-red-600 hover:bg-red-50 hover:text-red-900 dark:text-red-400 dark:hover:bg-red-900 dark:hover:text-red-300"
+ >
+ {isStopping() ? 'Stopping…' : 'Stop monitoring'}
+
+
+ }
+ >
+ handleAllowHostReconnect(row)}
+ disabled={Boolean(pendingAction())}
+ class="inline-flex min-h-10 sm:min-h-9 items-center rounded-md px-2.5 py-1.5 text-sm font-medium text-blue-600 hover:bg-blue-50 hover:text-blue-900 dark:text-blue-400 dark:hover:bg-blue-900 dark:hover:text-blue-300"
+ >
+ {isAllowingReconnect()
+ ? 'Allowing reconnect…'
+ : ALLOW_RECONNECT_LABEL}
+
+ }
+ >
+ handleAllowKubernetesReconnect(row)}
+ disabled={Boolean(pendingAction())}
+ class="inline-flex min-h-10 sm:min-h-9 items-center rounded-md px-2.5 py-1.5 text-sm font-medium text-blue-600 hover:bg-blue-50 hover:text-blue-900 dark:text-blue-400 dark:hover:bg-blue-900 dark:hover:text-blue-300"
+ >
+ {isAllowingReconnect()
+ ? 'Allowing reconnect…'
+ : ALLOW_RECONNECT_LABEL}
+
+
+ }
+ >
+ handleAllowDockerReconnect(row)}
+ disabled={Boolean(pendingAction())}
+ class="inline-flex min-h-10 sm:min-h-9 items-center rounded-md px-2.5 py-1.5 text-sm font-medium text-blue-600 hover:bg-blue-50 hover:text-blue-900 dark:text-blue-400 dark:hover:bg-blue-900 dark:hover:text-blue-300"
+ >
+ {isAllowingReconnect() ? 'Allowing reconnect…' : ALLOW_RECONNECT_LABEL}
+
+
+
+ );
+ },
+ },
+ ];
+
+ const renderSelectedActiveRowDetails = (
+ rowAccessor: () => UnifiedAgentRow,
+ ) => {
+ const row = () => rowAccessor();
+ const isKubernetes = () =>
+ row().capabilities.includes('kubernetes') && !row().capabilities.includes('agent');
+ const resolvedAgentId = () => row().agentId || '';
+ const assignment = () =>
+ resolvedAgentId() ? assignmentByAgent().get(resolvedAgentId()) : undefined;
+ const isScopeUpdating = () =>
+ resolvedAgentId() ? Boolean(pendingScopeUpdates()[resolvedAgentId()]) : false;
+ const agentName = () => row().displayName || row().hostname || row().name;
+ const surfaces = () => getRowSurfaceBreakdown(row());
+
+ const renderHeader = () => (
+
+
+
+
+
+ Selected reporting item
+
+
{row().name}
+
+ {row().hostname}
+
+
+ Use surface controls to stop specific reporting without removing the machine.
+
+
+
+
+ {(cap) => (
+
+ {getAgentCapabilityLabel(cap)}
+
+ )}
+
+
+
+ Outdated
+
+
+
+
+ Linked
+
+
+
+
+
setExpandedRowKey(null)}
+ class="rounded-md p-1 hover:bg-surface-hover hover:text-base-content"
+ aria-label="Close"
+ >
+
+
+
+
+
+
+ );
+
+ const renderMachineOverview = () => (
+
+
Machine overview
+
+
+
+ Item ID: {row().id}
+
+
+
+ Agent ID: {row().agentActionId}
+
+
+
+
+ Container Agent ID:{' '}
+ {row().dockerActionId}
+
+
+
+
+ Cluster ID:{' '}
+ {row().kubernetesActionId}
+
+
+
+
+ Reporting agent ID:{' '}
+ {row().agentId}
+
+
+
+
+ Linked node ID:{' '}
+ {row().linkedNodeId}
+
+
+
+
+
+
+ Last seen {formatRelativeTime(row().lastSeen!)} (
+ {formatAbsoluteTime(row().lastSeen!)})
+
+
+
+
+
Scope profile
+
+ {row().scope.label}
+
+ }
+ >
+ 0}
+ fallback={
+
+ {row().scope.label}
+
+ }
+ >
+
+ {
+ const nextValue = event.currentTarget.value;
+ const currentValue = assignment()?.profile_id || '';
+ if (nextValue === currentValue) {
+ return;
+ }
+ void updateScopeAssignment(
+ resolvedAgentId(),
+ nextValue || null,
+ agentName(),
+ );
+ }}
+ disabled={isScopeUpdating()}
+ class="min-h-10 sm:min-h-9 rounded-md border border-border bg-surface px-2.5 py-1.5 text-sm text-base-content shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:cursor-not-allowed disabled:opacity-60 dark:focus:border-blue-400 dark:focus:ring-blue-800"
+ >
+ Default (Auto-detect)
+
+
+ {getProfileOptionLabel(assignment()!.profile_id)}
+
+
+
+ {(profile) => (
+
+ {getProfileOptionLabel(profile.id)}
+
+ )}
+
+
+
+ Updating…
+
+
+
+ }
+ >
+
+ {row().scope.label}
+
+
+
+
+
+
+
+
+ Kubernetes connection
+
+
+
+ Server:{' '}
+ {row().kubernetesInfo?.server}
+
+
+
+
+ Context:{' '}
+ {row().kubernetesInfo?.context}
+
+
+
+
+ Token:{' '}
+ {row().kubernetesInfo?.tokenName}
+
+
+
+
+
+
+
+
+
+ Restart required to apply scope changes.
+
+
handleResetScope(resolvedAgentId(), agentName() || resolvedAgentId())}
+ class="mt-2 text-xs text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 text-left"
+ >
+ Reset to default
+
+
+
+
+ );
+
+ const renderSurfaceControls = () => (
+ 0}>
+
+
+
+ Surface controls
+
+
+ Stop a specific surface. Other surfaces keep reporting.
+
+
+
+
+
Surface
+
What Pulse receives
+
ID
+
Control
+
+
+ {(surface) => (
+
+
{surface.label}
+
{surface.detail}
+
+
Not separately addressed}
+ >
+
+
{surface.idLabel}
+
{surface.idValue}
+
+
+
+
+ Managed with host telemetry
+ }
+ >
+ {
+ event.stopPropagation();
+ openStopMonitoringDialog(
+ createSurfaceScopedRow(
+ row(),
+ surface.key as 'agent' | 'docker' | 'kubernetes',
+ ),
+ );
+ }}
+ class="inline-flex min-h-9 items-center rounded-md px-2.5 py-1.5 text-xs font-medium text-red-600 hover:bg-red-50 hover:text-red-900 dark:text-red-400 dark:hover:bg-red-900 dark:hover:text-red-300"
+ >
+ Stop this surface
+
+
+
+
+ )}
+
+
+
+
+ );
+
+ const renderMachineActions = () => (
+
+
Machine actions
+
Machine-level utilities.
+
+
+ {
+ const cmd = getPlatformUninstallCommand(row().upgradePlatform, row());
+ const success = await copyToClipboard(cmd);
+ if (success) {
+ notificationStore.success(getUnifiedAgentUninstallCommandCopiedMessage());
+ } else {
+ notificationStore.error(getUnifiedAgentClipboardCopyErrorMessage());
+ }
+ }}
+ class="rounded-md border border-border px-3 py-2 text-left text-xs text-slate-600 hover:bg-surface hover:text-base-content"
+ >
+ Copy uninstall command
+
+
+
+ {
+ const success = await copyToClipboard(getUpgradeCommand(row()));
+ if (success) {
+ notificationStore.success(getUnifiedAgentUpgradeCommandCopiedMessage());
+ } else {
+ notificationStore.error(getUnifiedAgentClipboardCopyErrorMessage());
+ }
+ }}
+ class="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-left text-xs text-amber-700 hover:bg-amber-100 hover:text-amber-900 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-300 dark:hover:bg-amber-900/60 dark:hover:text-amber-200"
+ >
+ Copy upgrade command
+
+
+
+ Use surface controls above to stop reporting without uninstalling.
+
+
+
+ );
+
+ return (
+
+ {renderHeader()}
+
+ {renderMachineOverview()}
+ {renderSurfaceControls()}
+ {renderMachineActions()}
+
+
+ );
+ };
+
+ const renderSelectedIgnoredRowDetails = (
+ rowAccessor: () => UnifiedAgentRow,
+ ) => {
+ const row = () => rowAccessor();
+ const pendingAction = () => getPendingInventoryAction(row().rowKey);
+ const isAllowingReconnect = () => pendingAction() === 'allow-reconnect';
+ const reconnectLabel = () => getReconnectActionLabel(row());
+ const blockedId = () =>
+ row().dockerActionId || row().kubernetesActionId || row().agentActionId || row().id;
+
+ const renderHeader = () => (
+
+
+
+
+ Selected ignored item
+
+
{row().name}
+
+ Ignored by Pulse
+
+
+ Pulse is blocking reports from this surface.
+
+
+
setSelectedIgnoredRowKey(null)}
+ class="rounded-md p-1 hover:bg-amber-200/70 hover:text-base-content dark:hover:bg-amber-800/50"
+ aria-label="Close"
+ >
+
+
+
+
+
+
+ );
+
+ const renderIgnoredSurface = () => (
+
+
Ignored surface
+
+
+
Ignored surface
+
What Pulse is ignoring
+
ID
+
Recovery
+
+
+
+ {row().capabilities.map(getCapabilitySurfaceLabel).join(', ')}
+
+
+ Pulse will ignore new reports for this surface until reconnect is allowed.
+
+
+ {blockedId()}
+
+
Ready to return
+
+
+
+ );
+
+ const renderRecoveryAction = () => (
+
+
Recovery action
+
Allow this blocked ID to report again.
+
+
+ row().capabilities.includes('docker')
+ ? handleAllowDockerReconnect(row())
+ : row().capabilities.includes('kubernetes')
+ ? handleAllowKubernetesReconnect(row())
+ : handleAllowHostReconnect(row())
+ }
+ disabled={Boolean(pendingAction())}
+ class="inline-flex min-h-10 sm:min-h-9 items-center justify-center rounded-md bg-white px-3 py-2 text-sm font-medium text-blue-600 shadow-sm ring-1 ring-border hover:bg-blue-50 hover:text-blue-900 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-slate-900 dark:text-blue-400 dark:ring-slate-700 dark:hover:bg-blue-900 dark:hover:text-blue-300"
+ >
+ {isAllowingReconnect() ? 'Allowing reconnect…' : reconnectLabel()}
+
+
+ This only changes Pulse. It does not reinstall software.
+
+
+
+ );
+
+ return (
+
+ {renderHeader()}
+
+ {renderIgnoredSurface()}
+ {renderRecoveryAction()}
+
+
+ );
+ };
+
+ const renderStopMonitoringDialog = () => (
+ {
+ if (!stopMonitoringDialog()) return;
+ const row = stopMonitoringDialog()!.row;
+ if (getPendingInventoryAction(row.rowKey)) return;
+ setStopMonitoringDialog(null);
+ }}
+ panelClass="max-w-lg"
+ closeOnBackdrop={
+ !stopMonitoringDialog() || !getPendingInventoryAction(stopMonitoringDialog()!.row.rowKey)
+ }
+ ariaLabel="Confirm stop monitoring"
+ >
+
+ {(dialog) => {
+ const row = () => dialog().row;
+ const pending = () => getPendingInventoryAction(row().rowKey) === 'stop-monitoring';
+ const isKubernetes = () =>
+ row().capabilities.includes('kubernetes') && !row().capabilities.includes('agent');
+ const affectedSurfaces = () => getStopMonitoringSurfaces(row());
+ return (
+
+
+
Stop monitoring?
+
+ Pulse will remove{' '}
+ {dialog().subject} from
+ active reporting.
+
+
+
+
+
{dialog().scopeLabel} will stop in Pulse.
+
+ The remote system keeps running. Pulse will ignore future reports and move this
+ item into Ignored by Pulse until you allow reconnect.
+
+
+
0}>
+
+
+ Pulse will stop these reporting surfaces
+
+
+
+ {(surface) => (
+
+
{surface.label}
+
{surface.detail}
+
+
+ {surface.idLabel}:{' '}
+ {surface.idValue}
+
+
+
+ )}
+
+
+
+
+
+
What stays unchanged
+
+ {isKubernetes()
+ ? 'The cluster itself is not uninstalled or shut down.'
+ : 'The host, containers, and installed agent binaries are not uninstalled or shut down.'}
+
+
+
+
+ setStopMonitoringDialog(null)}
+ disabled={pending()}
+ class="inline-flex min-h-10 sm:min-h-9 items-center justify-center rounded-md border border-border px-4 py-2 text-sm font-medium text-base-content hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-60"
+ >
+ Cancel
+
+
+ isKubernetes()
+ ? handleRemoveKubernetesCluster(row())
+ : handleRemoveAgent(row())
+ }
+ disabled={pending()}
+ class="inline-flex min-h-10 sm:min-h-9 items-center justify-center rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 disabled:cursor-not-allowed disabled:opacity-60"
+ >
+ {pending() ? 'Stopping…' : 'Confirm stop monitoring'}
+
+
+
+ );
+ }}
+
+
+ );
+
+ const renderInstallerSection = () => (
+ }
+ bodyClass="space-y-5"
+ >
+
+ {(handoff) => (
+
+
+
+
Security configured. Save these first-run credentials now.
+
+ This is the canonical handoff from first-run setup into Infrastructure Install.
+ Generate a scoped install token below before copying agent commands.
+
+
+
+
+ Username
+
+
{handoff().username}
+
+
+
+ Password
+
+
+ {handoff().password}
+
+
+
+
+ Admin API Token
+
+
+ {handoff().apiToken}
+
+
+
+
+
+
+ void copySetupHandoffField(handoff().password, 'Copied first-run password.')
+ }
+ class="inline-flex items-center justify-center rounded-md border border-emerald-300 bg-white px-3 py-2 text-sm font-medium text-emerald-900 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-100 dark:hover:bg-emerald-800"
+ >
+ Copy password
+
+
+ void copySetupHandoffField(
+ handoff().apiToken,
+ 'Copied first-run admin API token.',
+ )
+ }
+ class="inline-flex items-center justify-center rounded-md border border-emerald-300 bg-white px-3 py-2 text-sm font-medium text-emerald-900 hover:bg-emerald-100 dark:border-emerald-700 dark:bg-emerald-950 dark:text-emerald-100 dark:hover:bg-emerald-800"
+ >
+ Copy admin token
+
+
+ Download credentials
+
+
+ Dismiss
+
+
+
+
+ )}
+
+
+
+
Unified Agent is the default monitoring gateway.
+
+ Install it on each system you want Pulse to monitor. The installer auto-detects
+ available platforms on that machine and enables the right integrations.
+
+
+
+
+
+
+
+
+
+ Proxmox nodes can be added here with the unified agent for extra capabilities
+ like temperature monitoring and Pulse Patrol automation (auto-creates the
+ required token and links the node).
+
+
navigate('/settings/infrastructure/proxmox')}
+ class="mt-2 inline-flex min-h-10 sm:min-h-9 items-center rounded-md px-2 py-1.5 text-sm font-medium text-emerald-800 hover:bg-emerald-100 hover:text-emerald-900 dark:text-emerald-200 dark:hover:bg-emerald-900 dark:hover:text-emerald-100 underline"
+ >
+ Need direct setup instead? Open Proxmox →
+
+
+
+
+
+
+
+
+
+
+
+ 1
+
+ Generate API token
+
+
+ {requiresToken()
+ ? 'Create a fresh token scoped for Agent, Docker, and Kubernetes monitoring.'
+ : 'Tokens are optional on this Pulse instance. Generate one if you want copied commands to preserve explicit credentialed transport.'}
+
+
+
+
+ setTokenName(event.currentTarget.value)}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' && !isGeneratingToken()) {
+ handleGenerateToken();
+ }
+ }}
+ placeholder="Token name (optional)"
+ class="flex-1 rounded-md border border-border bg-surface px-3 py-2 text-sm text-base-content shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:focus:border-blue-400 dark:focus:ring-blue-900"
+ />
+
+ {isGeneratingToken()
+ ? 'Generating…'
+ : hasToken()
+ ? 'Generate another'
+ : 'Generate token'}
+
+
+
+
+
+
+
+
+
+ Token {latestRecord()?.name} created. Commands below now
+ include this credential.
+
+
+
+
+
+
+
+
+ Tokens are optional on this Pulse instance. Confirm to generate commands without
+ embedding a token.
+
+
+ {confirmedNoToken() ? 'No token confirmed' : 'Confirm without token'}
+
+
+
+
+
+
+
+
+
+
+ 2
+
+ Installation commands
+
+
+ Generate a token above to unlock installation commands.
+
+
+
+
+
+
+
+
+ Click "Generate token" above to see installation commands
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 2
+
+
+ Installation commands
+
+
+ The installer auto-detects Docker, Kubernetes, and Proxmox on the target
+ machine.
+
+
+
+
+
+
+ Connection URL (Agent → Pulse)
+
+
+ setCustomAgentUrl(e.currentTarget.value)}
+ placeholder={agentUrl()}
+ class="flex-1 rounded-md border bg-surface px-3 py-1.5 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-800"
+ />
+
+
+ Override the address agents use to connect to this server (e.g., use IP
+ address http://192.0.2.50:7655 if DNS fails).
+
+
+ Currently using auto-detected: {agentUrl()}
+
+
+
+
+
+
+ Custom CA certificate path (optional)
+
+
+ setCustomCaPath(e.currentTarget.value)}
+ placeholder={
+ selectedAgentUrl().startsWith('http://')
+ ? 'Not needed for plain HTTP'
+ : 'Examples: /etc/pulse/ca.pem or C:\\Pulse\\ca.cer'
+ }
+ class="flex-1 rounded-md border bg-surface px-3 py-1.5 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-800"
+ />
+
+
+ Preserves custom trust for copied install, upgrade, and uninstall commands.
+ Shell commands pass --cacert to both the download and the
+ installer. Windows commands set PULSE_CACERT and use a
+ transport-aware PowerShell bootstrap for the initial script fetch.
+
+
+
+
+ TLS verification disabled — skip cert checks
+ for self-signed setups. Not recommended for production.
+
+
+
+ setInsecureMode(e.currentTarget.checked)}
+ class="rounded text-blue-600 focus:ring-blue-500"
+ />
+ Skip TLS certificate verification (self-signed certs; not recommended)
+
+
+ setEnableCommands(e.currentTarget.checked)}
+ class="rounded text-blue-600 focus:ring-blue-500"
+ />
+ Enable Pulse command execution (for Patrol auto-fix)
+
+
+
+ Pulse commands enabled — The agent will
+ accept diagnostic and fix commands from Pulse Patrol features.
+
+
+
+ Config signing (optional) — Require signed
+ remote config payloads with{' '}
+ PULSE_AGENT_CONFIG_SIGNATURE_REQUIRED=true. Provide keys via{' '}
+ PULSE_AGENT_CONFIG_SIGNING_KEY (Pulse) and{' '}
+ PULSE_AGENT_CONFIG_PUBLIC_KEYS (agents).
+
+
+
+ Target profile (optional)
+
+
+ handleInstallProfileChange(event.currentTarget.value as InstallProfile)
+ }
+ class="w-full rounded-md border bg-surface px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-800"
+ >
+
+ {(option) => {option.label} }
+
+
+
+ {getSelectedInstallProfile().description}
+
+
0}>
+
+ Adds flags to shell-based install commands:{' '}
+ {getInstallProfileFlags().join(' ')}
+
+
+
+
+
+
+
+ {(section) => (
+
+
+
{section.title}
+
{section.description}
+
+
+
+ {(snippet) => {
+ const copyCommand = () => snippet.command;
+ const commandTelemetryCapability = () => {
+ const label = normalizeTelemetryPart(snippet.label) || 'install';
+ return `${section.platform}:${installProfile()}:${label}`;
+ };
+
+ return (
+
+
+ {snippet.label}
+
+
+
{
+ const success = await copyToClipboard(copyCommand());
+ if (success) {
+ trackAgentInstallCommandCopied(
+ UNIFIED_AGENT_TELEMETRY_SURFACE,
+ commandTelemetryCapability(),
+ );
+ notificationStore.success(
+ getUnifiedAgentClipboardCopySuccessMessage(),
+ );
+ } else {
+ notificationStore.error(
+ getUnifiedAgentClipboardCopyErrorMessage(),
+ );
+ }
+ }}
+ class="absolute right-2 top-2 inline-flex min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 items-center justify-center rounded-md bg-surface-hover p-2 transition-colors hover:text-slate-200"
+ title="Copy command"
+ >
+
+
+
+
+
+
+ {copyCommand()}
+
+
+
+ {snippet.note}
+
+
+ );
+ }}
+
+
+
+ )}
+
+
+
+
+
+
Check installation status
+
+ {lookupLoading() ? 'Checking…' : 'Check status'}
+
+
+
+ Enter the hostname (or agent ID) from the machine you just installed. Pulse
+ returns the latest status instantly.
+
+
+ {
+ setLookupValue(event.currentTarget.value);
+ setLookupError(null);
+ setLookupResult(null);
+ }}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ event.preventDefault();
+ void handleLookup();
+ }
+ }}
+ placeholder="Hostname or agent ID"
+ class="flex-1 rounded-md border border-blue-200 bg-surface px-3 py-2 text-sm text-blue-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-blue-700 dark:bg-blue-900 dark:text-blue-100 dark:focus:border-blue-300 dark:focus:ring-blue-800"
+ />
+
+
+
+ {lookupError()}
+
+
+
+ {(result) => {
+ const agent = () => result().agent!;
+ const lookupStatusPresentation = () =>
+ getUnifiedAgentLookupStatusPresentation(agent().connected);
+ return (
+
+
+
+ {agent().displayName || agent().hostname}
+
+
+
+ {lookupStatusPresentation().label}
+
+
+ {agent().status || 'unknown'}
+
+
+
+
+ Last seen {formatRelativeTime(agent().lastSeen)} (
+ {formatAbsoluteTime(agent().lastSeen)})
+
+
+
+ Agent version {agent().agentVersion}
+
+
+
+ );
+ }}
+
+
+
+
+ Troubleshooting
+
+
+
+
+ Auto-detection not working?
+
+
+ If Docker, Kubernetes, or Proxmox isn't detected automatically, add these
+ flags to the install command:
+
+
+
+ --enable-docker — Force
+ enable Docker/Podman monitoring
+
+
+ --enable-kubernetes —
+ Force enable Kubernetes monitoring
+
+
+ --enable-proxmox — Force
+ enable Proxmox integration (creates API token)
+
+
+ --proxmox-type pve|pbs{' '}
+ — Set Proxmox node mode explicitly
+
+
+ --disable-docker — Skip
+ Docker even if detected
+
+
+
+
+
+
+
+
+
+
+
Uninstall agent
+
+ Run the appropriate command on your machine to remove the Pulse agent:
+
+
+
Linux / macOS / FreeBSD
+
+
{
+ const success = await copyToClipboard(getUninstallCommand());
+ if (success) {
+ notificationStore.success(getUnifiedAgentClipboardCopySuccessMessage());
+ } else {
+ notificationStore.error(getUnifiedAgentClipboardCopyErrorMessage());
+ }
+ }}
+ class="absolute right-2 top-2 inline-flex min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 items-center justify-center rounded-md bg-surface-hover p-2 text-slate-400 transition-colors hover:bg-slate-700 hover:text-slate-200"
+ title="Copy command"
+ >
+
+
+
+
+
+
+ {getUninstallCommand()}
+
+
+
+
+ If the agent can't reach this server, run directly on the machine:{' '}
+
+ sudo bash /var/lib/pulse-agent/install.sh --uninstall
+ {' '}
+ (TrueNAS:{' '}
+
+ /data/pulse-agent/install.sh
+
+ , Unraid:{' '}
+
+ /boot/config/plugins/pulse-agent/install.sh
+
+ )
+
+
+
+ Windows (PowerShell as Administrator)
+
+
+
{
+ const success = await copyToClipboard(getWindowsUninstallCommand());
+ if (success) {
+ notificationStore.success(getUnifiedAgentClipboardCopySuccessMessage());
+ } else {
+ notificationStore.error(getUnifiedAgentClipboardCopyErrorMessage());
+ }
+ }}
+ class="absolute right-2 top-2 inline-flex min-h-10 sm:min-h-9 min-w-10 sm:min-w-9 items-center justify-center rounded-md bg-surface-hover p-2 text-slate-400 transition-colors hover:bg-slate-700 hover:text-slate-200"
+ title="Copy command"
+ >
+
+
+
+
+
+
+ {getWindowsUninstallCommand()}
+
+
+
+
+
+
+
+ );
+
+ const renderIgnoredSection = () => (
+
+
+
}
+ bodyClass="space-y-4"
+ >
+
0}
+ fallback={
+
+ {getMonitoringStoppedEmptyState(hasFilters())}
+
+ }
+ >
+
+
+
+
+ Browse ignored items
+
+
+ Select an ignored item to open its recovery drawer.
+
+
+
+
+ {(row) => {
+ const pendingAction = () => getPendingInventoryAction(row.rowKey);
+ const isSelected = () => selectedIgnoredRowKey() === row.rowKey;
+ return (
+ setSelectedIgnoredRowKey(row.rowKey)}
+ class={`flex w-full flex-col gap-2 px-4 py-3 text-left transition-colors ${
+ isSelected()
+ ? 'bg-amber-100/80 ring-1 ring-inset ring-amber-300 dark:bg-amber-900/40 dark:ring-amber-700'
+ : 'hover:bg-amber-100/50 dark:hover:bg-amber-900/20'
+ }`}
+ >
+
+
+
+
+ {row.name}
+
+
+ {getRemovedUnifiedAgentItemLabel(row)}
+
+
+
+ {row.capabilities.map(getCapabilitySurfaceLabel).join(', ')}
+
+
+
+ {pendingAction() === 'allow-reconnect'
+ ? 'Reconnect in progress'
+ : 'Select to review'}
+
+
+
+
+ Hostname: {row.hostname}
+
+
+ Stopped{' '}
+ {row.removedAt
+ ? `${formatRelativeTime(row.removedAt)} (${formatAbsoluteTime(row.removedAt)})`
+ : 'at an unknown time'}
+
+
+
+ );
+ }}
+
+
+
+
+
setSelectedIgnoredRowKey(null)}
+ layout="drawer-right"
+ panelClass="max-w-[720px]"
+ ariaLabel="Ignored item details"
+ >
+
+ {(rowAccessor) => renderSelectedIgnoredRowDetails(rowAccessor)}
+
+
+
+
+
+
+
+ );
+
+ const renderInventorySection = () => (
+
+
+
{inventoryStatusSummaryText()}
+
+
+
}
+ bodyClass="space-y-4"
+ >
+
+
{reportingCoverageSummaryText()}
+
+ This workspace does not list every asset Pulse has discovered. It focuses on systems
+ and runtimes that are actively checking in right now.
+
+
+
+
+
+ Active reporting
+
+
+ {filteredActiveRows().length}
+
+
+ Item{filteredActiveRows().length === 1 ? '' : 's'} actively checking in to Pulse.
+
+
+
+
+
+
+
+
+
+ {linkedAgents().length} agent
+ {linkedAgents().length > 1 ? 's are' : ' is'} linked to Proxmox node
+ {linkedAgents().length > 1 ? 's' : ''} and flagged with a{' '}
+ Linked badge.
+
+
+
+
+
+
+
+
+
+
+
+
+ {outdatedAgents().length} outdated agent{' '}
+ {outdatedAgents().length > 1 ? 'binaries' : 'binary'} detected
+
+
+ Older standalone agent binaries are deprecated. Expand a row to copy the upgrade
+ command.
+
+
+
+
+
+
+
+
+
+ Search reporting items
+
+
+
+
+
+
+ Refine results
+
+
+
+ Capability
+
+
+ setFilterCapability(event.currentTarget.value as 'all' | AgentCapability)
+ }
+ class="min-h-10 sm:min-h-9 rounded-md border border-border bg-surface px-2.5 py-2 sm:py-1.5 text-sm text-base-content shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-800"
+ >
+ All capabilities
+ Agent
+ Docker
+ Kubernetes
+ Proxmox
+ PBS
+ PMG
+
+
+
+
+ Scope
+
+
+ setFilterScope(
+ event.currentTarget.value as 'all' | Exclude,
+ )
+ }
+ class="min-h-10 sm:min-h-9 rounded-md border border-border bg-surface px-2.5 py-2 sm:py-1.5 text-sm text-base-content shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-800"
+ >
+ All scopes
+ Default
+ Profile assigned
+ Patrol-managed
+
+
+
+ Clear
+
+
+
+
+
+
+ Showing {filteredActiveRows().length} of {activeRows().length} active records.
+
+ 0}>
+
+ {filteredMonitoringStoppedRows().length} item(s) are currently ignored by Pulse.
+
+
+
+
+
+ Stop monitoring removes an item from active reporting and moves it into the Ignored by
+ Pulse list. The remote system keeps running; Pulse simply ignores new reports until you
+ allow reconnect.
+
+
+
+ {(notice) => (
+
+
+
+
{notice().title}
+
{notice().detail}
+
+
+
+
+ View ignored items
+
+
+ setInventoryActionNotice(null)}
+ class="inline-flex min-h-10 sm:min-h-9 items-center rounded-md px-2.5 py-1.5 text-xs font-medium underline"
+ aria-label="Dismiss inventory action message"
+ >
+ Dismiss
+
+
+
+
+ )}
+
+
+
+
+
+
+ Browse reporting items
+
+
+ Select a reporting item to open its details drawer.
+
+
+
row.rowKey}
+ onRowClick={(row) => toggleAgentDetails(row.rowKey)}
+ />
+
+
+
setExpandedRowKey(null)}
+ layout="drawer-right"
+ panelClass="max-w-[760px]"
+ ariaLabel="Reporting item details"
+ >
+
+ {(rowAccessor) => renderSelectedActiveRowDetails(rowAccessor)}
+
+
+
+
+
+ {renderIgnoredSection()}
+
+ );
+
+ return {
+ renderInstallerSection,
+ renderStopMonitoringDialog,
+ renderInventorySection,
+ };
+};
+
+export type InfrastructureOperationsState = ReturnType;