diff --git a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx index 9a26f106d..24566653e 100644 --- a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx +++ b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx @@ -33,6 +33,7 @@ import { } from '@/utils/resourceAnalysisPresentation'; import { useDiscoveryTabState } from './useDiscoveryTabState'; import { orderFactsByActionability } from './factOrdering'; +import { deriveCliCommand } from './cliCommand'; interface DiscoveryTabProps { resourceType: ResourceType; @@ -151,6 +152,14 @@ export const DiscoveryTab: Component = (props) => { } return getConfidenceLevel(current.confidence); }); + // A concrete, human-runnable command for this workload (pct exec / docker + // exec, including the nested-container layer). null when there is no clean + // human command for the type, in which case we show the Assistant guidance. + const cliCommand = createMemo(() => { + const current = discovery(); + if (!current) return null; + return deriveCliCommand(props.resourceType, current.resource_id, current.cli_access); + }); const commandSettingsTarget = getDiscoveryCommandSettingsTarget(); const apiAccessSettingsTarget = getDiscoveryApiAccessSettingsTarget(); const showManualRunAction = () => props.showManualRunAction === true; @@ -824,12 +833,26 @@ export const DiscoveryTab: Component = (props) => { CLI Access - + + How the Pulse Assistant runs commands here: + {d().cli_access} +

+ } + > + +

+ Run from the{' '} + {props.resourceType === 'app-container' ? 'Docker host' : 'Proxmox host'}. +

+
diff --git a/frontend-modern/src/components/Discovery/cliCommand.test.ts b/frontend-modern/src/components/Discovery/cliCommand.test.ts new file mode 100644 index 000000000..93cbcc444 --- /dev/null +++ b/frontend-modern/src/components/Discovery/cliCommand.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { deriveCliCommand } from './cliCommand'; + +describe('deriveCliCommand', () => { + it('derives pct exec for a native LXC', () => { + expect( + deriveCliCommand('system-container', '101', 'Use pulse_control ... directly inside the container.'), + ).toBe('pct exec 101 -- bash'); + }); + + it('layers docker exec for a service in a nested container (HA-in-LXC)', () => { + const cliAccess = + 'Use pulse_control ... The service runs inside a Docker container named "homeassistant" ' + + '— prefix commands with: docker exec homeassistant .'; + expect(deriveCliCommand('system-container', '101', cliAccess)).toBe( + 'pct exec 101 -- docker exec homeassistant bash', + ); + }); + + it('uses docker exec for a Docker container', () => { + expect(deriveCliCommand('app-container', 'redis', undefined)).toBe('docker exec redis bash'); + }); + + it('returns null for types without a clean human command (VM, k8s, agent)', () => { + expect(deriveCliCommand('vm', '200', 'Use pulse_control ... inside the VM.')).toBeNull(); + expect(deriveCliCommand('pod', 'web-0', 'Use kubectl exec -n default web-0 -- ')).toBeNull(); + expect(deriveCliCommand('agent', 'node1', 'Use pulse_control ...')).toBeNull(); + }); + + it('returns null when the resource id is missing', () => { + expect(deriveCliCommand('system-container', '', 'x')).toBeNull(); + }); +}); diff --git a/frontend-modern/src/components/Discovery/cliCommand.ts b/frontend-modern/src/components/Discovery/cliCommand.ts new file mode 100644 index 000000000..cd67c8ad0 --- /dev/null +++ b/frontend-modern/src/components/Discovery/cliCommand.ts @@ -0,0 +1,37 @@ +import type { ResourceType } from '../../types/discovery'; + +/** + * Derives a concrete, human-runnable command for reaching a workload from its + * resource coordinates. + * + * The discovery `cli_access` field is guidance written for the Pulse Assistant + * ("Use pulse_control with target_host …") — it is not something a person types. + * This returns what a human would actually run (e.g. `pct exec 101 -- bash`), + * including the nested-container layer when the service runs in Docker inside an + * LXC/VM. Returns `null` when there is no clean human command for the type (the + * caller then falls back to showing the guidance text, e.g. k8s kubectl which is + * already human-readable, or VMs where SSH/credentials are the real path). + */ +export function deriveCliCommand( + resourceType: ResourceType, + resourceId: string, + cliAccess: string | undefined, +): string | null { + const id = (resourceId || '').trim(); + if (!id) return null; + + // A service running in a nested Docker container changes the access path; the + // backend records the container in cli_access as "docker exec …". + const nested = /docker exec (\S+)/.exec(cliAccess || '')?.[1]; + + switch (resourceType) { + case 'system-container': + return nested ? `pct exec ${id} -- docker exec ${nested} bash` : `pct exec ${id} -- bash`; + case 'app-container': + return `docker exec ${id} bash`; + default: + // vm (qm guest exec is non-interactive; SSH is the real path), pod (the + // guidance already shows a runnable kubectl exec), agent, etc. + return null; + } +}