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;
+ }
+}