diff --git a/README.md b/README.md index 28afdf3..966bc53 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- + @@ -18,12 +18,20 @@ > ⚠️ **AVISO CRÍTICO:** Aplicação em estágio inicial de desenvolvimento. Não use em produção — há risco de perda de dados. -Versão atual: **0.2.1** +Versão atual: **0.2.2** --- ## 📋 Changelog +### [0.2.2] — 2026-05-12 + +#### Corrigido +- **Volumes de container remoto:** ao clicar em um container para selecionar volumes no modal de criação de perfil, a busca de mounts agora utiliza a conexão correta (TCP remoto) quando a origem selecionada é uma conexão Docker remota (porta 2375), corrigindo o erro `ENOENT /var/run/docker.sock` que ocorria ao usar origens remotas. +- **Agrupamento por Docker Compose na lista de containers:** containers do modal de criação de profile agora são agrupados visualmente por projeto Compose, com cabeçalho identificando o stack. Containers sem Compose aparecem em grupo separado "Containers avulsos". + +--- + ### [0.2.1] — 2026-05-12 #### Adicionado diff --git a/package.json b/package.json index 1546360..f0deff6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dockerbackup-app", - "version": "0.2.1", + "version": "0.2.2", "description": "Aplicacao web para backup e restauracao de volumes Docker", "main": "src/server.js", "scripts": { diff --git a/public/app.js b/public/app.js index e6a7408..2f46175 100644 --- a/public/app.js +++ b/public/app.js @@ -1048,6 +1048,20 @@ function isMountBlocked(destination) { ); } +function renderContainerOption(container, selected) { + const hasVolumeSelection = Boolean(state.volumeSelections[container.id]?.length); + return ` + + `; +} + function renderContainers() { elements.containerCount.textContent = String(state.containers.length); @@ -1059,19 +1073,48 @@ function renderContainers() { } const selected = new Set(getSelectedContainerIds()); - elements.containerOptions.innerHTML = eligible.map((container) => { - const hasVolumeSelection = Boolean(state.volumeSelections[container.id]?.length); - return ` - - `; - }).join(''); + + // Separate compose containers from standalone + const composeGroups = new Map(); // project -> container[] + const standalone = []; + + for (const container of eligible) { + if (container.composeProject) { + if (!composeGroups.has(container.composeProject)) composeGroups.set(container.composeProject, []); + composeGroups.get(container.composeProject).push(container); + } else { + standalone.push(container); + } + } + + const parts = []; + + // Compose groups first, sorted by project name + for (const [project, containers] of [...composeGroups.entries()].sort(([a], [b]) => a.localeCompare(b))) { + parts.push(`

+
+ + ${escapeHtml(project)} +
+ ${containers.map((c) => renderContainerOption(c, selected)).join('')} +
`); + } + + // Standalone containers + if (standalone.length) { + const standaloneHeader = composeGroups.size > 0 + ? `
+
+ + Containers avulsos +
+ ${standalone.map((c) => renderContainerOption(c, selected)).join('')} +
` + : standalone.map((c) => renderContainerOption(c, selected)).join(''); + parts.push(standaloneHeader); + } + + elements.containerOptions.innerHTML = parts.join(''); } function backupButtons(profile) { @@ -1104,10 +1147,13 @@ function getProfileScopeLabel(scope) { return scope === 'container' ? 'container inteiro' : 'somente volumes'; } -async function askVolumeSelection(containerId, containerName, currentSelections) { +async function askVolumeSelection(containerId, containerName, currentSelections, sourceId) { let mounts; try { - mounts = await api(`/api/containers/${encodeURIComponent(containerId)}/mounts`); + const mountsUrl = sourceId + ? `/api/containers/${encodeURIComponent(containerId)}/mounts?sourceId=${encodeURIComponent(sourceId)}` + : `/api/containers/${encodeURIComponent(containerId)}/mounts`; + mounts = await api(mountsUrl); } catch (error) { showToast(`Falha ao buscar volumes de ${containerName}: ${error.message}`, true); return null; @@ -1224,7 +1270,8 @@ async function handleContainerCheck(event) { } const currentSelections = state.volumeSelections[containerId] || null; - const selected = await askVolumeSelection(containerId, containerName, currentSelections); + const sourceId = elements.profileSourceSelect?.value || null; + const selected = await askVolumeSelection(containerId, containerName, currentSelections, sourceId); if (selected === null) { input.checked = false; diff --git a/public/styles.css b/public/styles.css index 6b05548..f83b786 100644 --- a/public/styles.css +++ b/public/styles.css @@ -639,6 +639,29 @@ button, input, select { padding: 6px; } +.compose-group { + display: grid; + gap: 4px; +} + +.compose-group-header { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 6px 3px; + font-size: 11.5px; + font-weight: 700; + letter-spacing: 0.03em; + text-transform: uppercase; + color: var(--accent, #4a6cf7); + border-bottom: 1px solid var(--border); + margin-bottom: 2px; +} + +.compose-group-header--standalone { + color: var(--text-muted); +} + .container-option { display: grid; grid-template-columns: auto 1fr auto; diff --git a/src/dockerService.js b/src/dockerService.js index 919ccc5..4844e54 100644 --- a/src/dockerService.js +++ b/src/dockerService.js @@ -122,6 +122,8 @@ class DockerService { image: container.Image, state: container.State, status: container.Status, + composeProject: (container.Labels || {})['com.docker.compose.project'] || null, + composeService: (container.Labels || {})['com.docker.compose.service'] || null, })) .sort((left, right) => left.name.localeCompare(right.name)); } diff --git a/src/server.js b/src/server.js index 5bac235..021ddd1 100644 --- a/src/server.js +++ b/src/server.js @@ -223,7 +223,12 @@ async function main() { app.get('/api/containers/:containerId/mounts', authMiddleware, async (request, response) => { try { - const inspect = await dockerService.inspectContainer(request.params.containerId); + let ds = dockerService; + if (request.query.sourceId) { + const source = await store.getSource(String(request.query.sourceId)); + if (source) ds = createDockerServiceForSource(source); + } + const inspect = await ds.inspectContainer(request.params.containerId); const mounts = (inspect.Mounts || []) .filter((m) => m.Type === 'bind' || m.Type === 'volume') .map((m) => ({