Render Docker bind mounts in the Discovery tab

The backend captures DockerMounts (and now surfaces them to the
assistant), but the human-facing DiscoveryTab showed config/data/log
paths and ports while dropping the host<->container bind mounts — so a
user could not see where a container path like /config actually lives on
the host to edit or back up files. Add docker_mounts to the frontend
ResourceDiscovery type and render a 'Bind Mounts' card (host source
copyable, '-> destination in container', read-only marker), consistent
with the Ports card.

Test renders an app-container discovery with a bind mount and asserts
the host source is visible. Type-check + full lint + prettier clean;
full suite green (2 pre-existing guardrail failures unrelated).
This commit is contained in:
rcourtman
2026-06-07 12:55:56 +01:00
parent 2440138858
commit 3fa33448a7
3 changed files with 93 additions and 0 deletions
@@ -904,6 +904,38 @@ export const DiscoveryTab: Component<DiscoveryTabProps> = (props) => {
</div>
</Show>
{/* Docker bind mounts — host ↔ container path mapping, only on
Docker resources. The host source is where to actually edit or
back up files; the container destination is what the app sees. */}
<Show when={(d().docker_mounts?.length ?? 0) > 0}>
<div class="rounded border border-border p-3 shadow-sm">
<div class="mb-2 flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-base-content">
<span>Bind Mounts</span>
<DiscoveryProvenanceMarker showLabel={false} />
</div>
<div class="space-y-1.5">
<For each={d().docker_mounts}>
{(mount) => (
<div class="space-y-0.5">
<CopyableCodeRow
value={mount.source}
copiedValue={copiedDiscoveryValue}
onCopy={handleCopyDiscoveryValue}
label="Copy host path"
/>
<div class="pl-1 text-[10px] text-muted">
→ {mount.destination} in container
<Show when={mount.read_only}>
<span> (read-only)</span>
</Show>
</div>
</div>
)}
</For>
</div>
</div>
</Show>
{/* Ports */}
<Show when={d().ports?.length > 0}>
<div class="rounded border border-border p-3 shadow-sm">
@@ -354,4 +354,54 @@ describe('DiscoveryTab', () => {
await screen.findByText('Mapped open ports and running processes to a PostgreSQL service.'),
).toBeInTheDocument();
});
it('renders Docker bind mounts so the host path is visible', async () => {
vi.mocked(discoveryApi.getDiscoveryInfo).mockResolvedValue(discoveryInfoWithProvider());
vi.mocked(discoveryApi.getDiscovery).mockResolvedValue({
id: 'app-container:nuc:homeassistant',
resource_type: 'app-container',
resource_id: 'homeassistant',
target_id: 'nuc',
hostname: 'nuc',
service_type: 'home-assistant',
service_name: 'Home Assistant',
service_version: '2026.5',
category: 'home_automation',
cli_access: 'docker exec homeassistant bash',
facts: [],
config_paths: ['/config/automations.yaml'],
data_paths: [],
log_paths: [],
ports: [],
docker_mounts: [
{
container_name: 'homeassistant',
source: '/opt/homeassistant/config',
destination: '/config',
type: 'bind',
},
],
user_notes: '',
user_secrets: {},
confidence: 0.9,
ai_reasoning: '',
discovered_at: '2026-05-01T00:00:00Z',
updated_at: '2026-05-01T00:00:00Z',
scan_duration: 10,
});
render(() => (
<DiscoveryTab
resourceType="app-container"
agentId="nuc"
resourceId="homeassistant"
hostname="nuc"
/>
));
// The host source is where you actually edit/back up the files — it must be
// visible, not just the container-internal /config path.
expect(await screen.findByText('Bind Mounts')).toBeInTheDocument();
expect(await screen.findByText('/opt/homeassistant/config')).toBeInTheDocument();
});
});
+11
View File
@@ -48,6 +48,16 @@ export interface PortInfo {
address: string;
}
export interface DockerBindMount {
container_name?: string;
/** Host path — where the container's files actually live. */
source: string;
/** Container path — what the service sees. */
destination: string;
type?: string;
read_only?: boolean;
}
export interface ResourceDiscovery {
id: string;
resource_type: APIResourceType;
@@ -67,6 +77,7 @@ export interface ResourceDiscovery {
data_paths: string[];
log_paths: string[];
ports: PortInfo[];
docker_mounts?: DockerBindMount[];
user_notes: string;
user_secrets: Record<string, string>;
confidence: number;