atualiza versão para 0.1.5; adiciona botão de log na aba Backup Runs, novo endpoint para detalhes de backup e melhora a exibição do changelog

This commit is contained in:
Alexander Sabino 2026-05-10 10:45:37 +01:00
parent 83e7490654
commit 9f7eacecb9
7 changed files with 192 additions and 8 deletions

View File

@ -1,4 +1,4 @@
<p align="center"> <p align="center">
<img src="./public/docker_backup_icon_for_appstore.png" width="200" alt="DockerBackup" /> <img src="./public/docker_backup_icon_for_appstore.png" width="200" alt="DockerBackup" />
</p> </p>
@ -9,7 +9,7 @@
</p> </p>
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/VERSION-0.1.4-blue?style=flat-square" /> <img src="https://img.shields.io/badge/VERSION-0.1.5-blue?style=flat-square" />
<img src="https://img.shields.io/badge/NODE.JS-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white" /> <img src="https://img.shields.io/badge/NODE.JS-%3E%3D20-339933?style=flat-square&logo=node.js&logoColor=white" />
<img src="https://img.shields.io/badge/DOCKER-ready-2496ED?style=flat-square&logo=docker&logoColor=white" /> <img src="https://img.shields.io/badge/DOCKER-ready-2496ED?style=flat-square&logo=docker&logoColor=white" />
<img src="https://img.shields.io/badge/READY-yes-brightgreen?style=flat-square" /> <img src="https://img.shields.io/badge/READY-yes-brightgreen?style=flat-square" />
@ -23,6 +23,18 @@ Versão atual: **0.1.4**
--- ---
## <20> Changelog ## <20> Changelog
### [0.1.5] — 2026-05-10
#### Adicionado
- **Botão `Log` na aba Backup Runs:** cada run exibe agora um botão que abre um modal com o log completo do backup por container, incluindo saída do tar, avisos e erros ocorridos durante a execução.
- **Endpoint `GET /api/backups/:backupId`:** novo endpoint que retorna os dados completos de um backup (incluindo logs) a partir do seu ID.
- **Logs persistidos por container no backup:** o array de logs gerado durante o backup (`pushLog`) é agora salvo em `containerBackup.logs` e persistido no `store.json`, disponível para consulta posterior.
#### Melhorado
- **Dropdowns estilizados como campos de texto:** todos os elementos `<select>` dentro de `.form-field` e o `.settings-select` passaram a usar o mesmo estilo visual dos inputs de texto (borda, padding, tipografia), com ícone de seta customizado via SVG.
- **Tela Sobre — posição do autor:** o texto `Desenvolvido por Alexander Sabino em 2026` foi movido para logo abaixo da descrição da aplicação.
- **Tela Sobre — changelog limitado:** a seção de changelog exibe agora apenas as últimas 4 entradas de versão.
### [0.1.4] — 2026-05-09 ### [0.1.4] — 2026-05-09
#### Corrigido #### Corrigido

View File

@ -1,6 +1,6 @@
{ {
"name": "dockerbackup-app", "name": "dockerbackup-app",
"version": "0.1.4", "version": "0.1.5",
"description": "Aplicacao web para backup e restauracao de volumes Docker", "description": "Aplicacao web para backup e restauracao de volumes Docker",
"main": "src/server.js", "main": "src/server.js",
"scripts": { "scripts": {

View File

@ -635,6 +635,7 @@ async function loadAllRuns() {
<td>${fileCount || '—'}</td> <td>${fileCount || '—'}</td>
<td>${sizeStr}</td> <td>${sizeStr}</td>
<td>${escapeHtml(new Date(b.createdAt).toLocaleString('pt-BR'))}</td> <td>${escapeHtml(new Date(b.createdAt).toLocaleString('pt-BR'))}</td>
<td><button class="btn btn--secondary btn--sm run-log-btn" data-backup-id="${escapeHtml(b.id)}">Log</button></td>
</tr> </tr>
`; `;
}).join(''); }).join('');
@ -1818,7 +1819,7 @@ function markdownInline(raw) {
.replace(/`([^`]+)`/g, '<code>$1</code>'); .replace(/`([^`]+)`/g, '<code>$1</code>');
} }
function parseChangelogSection(markdown) { function parseChangelogSection(markdown, maxEntries = 4) {
// Extract from ## Changelog (or ## 🗂 Changelog etc.) to next ## // Extract from ## Changelog (or ## 🗂 Changelog etc.) to next ##
const start = markdown.search(/^##\s+[^\n]*[Cc]hangelog/m); const start = markdown.search(/^##\s+[^\n]*[Cc]hangelog/m);
if (start === -1) return null; if (start === -1) return null;
@ -1829,6 +1830,7 @@ function parseChangelogSection(markdown) {
const lines = section.split('\n'); const lines = section.split('\n');
let html = ''; let html = '';
let inList = false; let inList = false;
let entryCount = 0;
for (const line of lines) { for (const line of lines) {
const h3 = line.match(/^###\s+(.+)/); const h3 = line.match(/^###\s+(.+)/);
@ -1836,12 +1838,16 @@ function parseChangelogSection(markdown) {
const li = line.match(/^-\s+(.+)/); const li = line.match(/^-\s+(.+)/);
if (h3) { if (h3) {
if (entryCount >= maxEntries) break;
if (inList) { html += '</ul>'; inList = false; } if (inList) { html += '</ul>'; inList = false; }
html += `<h4>${markdownInline(h3[1].replace(/\[([^\]]+)\]/, '$1'))}</h4>`; html += `<h4>${markdownInline(h3[1].replace(/\[([^\]]+)\]/, '$1'))}</h4>`;
entryCount += 1;
} else if (h4) { } else if (h4) {
if (entryCount > maxEntries) break;
if (inList) { html += '</ul>'; inList = false; } if (inList) { html += '</ul>'; inList = false; }
html += `<h5 class="changelog-section">${markdownInline(h4[1])}</h5>`; html += `<h5 class="changelog-section">${markdownInline(h4[1])}</h5>`;
} else if (li) { } else if (li) {
if (entryCount > maxEntries) break;
if (!inList) { html += '<ul>'; inList = true; } if (!inList) { html += '<ul>'; inList = true; }
html += `<li>${markdownInline(li[1])}</li>`; html += `<li>${markdownInline(li[1])}</li>`;
} else if (line.startsWith('---') || line.startsWith('## ')) { } else if (line.startsWith('---') || line.startsWith('## ')) {
@ -1939,6 +1945,60 @@ elements.profilesList.addEventListener('click', handleProfileAction);
document.querySelector('#backupsViewList').addEventListener('click', handleProfileAction); document.querySelector('#backupsViewList').addEventListener('click', handleProfileAction);
document.querySelector('#refreshRuns')?.addEventListener('click', () => loadAllRuns()); document.querySelector('#refreshRuns')?.addEventListener('click', () => loadAllRuns());
// Run log modal
document.querySelector('#allRunsBody')?.addEventListener('click', async (e) => {
const btn = e.target.closest('.run-log-btn');
if (!btn) return;
const backupId = btn.dataset.backupId;
if (!backupId) return;
openRunLogModal(backupId);
});
document.querySelector('#runLogModalClose')?.addEventListener('click', () => {
document.querySelector('#runLogModal')?.classList.add('hidden');
});
document.querySelector('#runLogModal')?.addEventListener('click', (e) => {
if (e.target.dataset.action === 'close-run-log-modal') {
document.querySelector('#runLogModal').classList.add('hidden');
}
});
async function openRunLogModal(backupId) {
const modal = document.querySelector('#runLogModal');
const content = document.querySelector('#runLogContent');
if (!modal || !content) return;
modal.classList.remove('hidden');
content.innerHTML = '<p class="changelog-loading">Carregando log...</p>';
try {
const backup = await api(`/api/backups/${encodeURIComponent(backupId)}`);
const containers = backup.containers || [];
if (!containers.length) {
content.innerHTML = '<p class="changelog-loading">Nenhum log disponível para este backup.</p>';
return;
}
let html = '';
for (const c of containers) {
const name = escapeHtml(c.containerName || c.containerId || '?');
const status = escapeHtml(c.status || '—');
html += `<div class="run-log-section">`;
html += `<h4 class="run-log-container-name">${name} <span class="status-badge status-badge--${escapeHtml(c.status || '')}">${status}</span></h4>`;
if (c.error) {
html += `<div class="run-log-error"><strong>Erro:</strong> ${escapeHtml(c.error)}</div>`;
}
const logs = c.logs || [];
if (logs.length) {
html += `<pre class="run-log-pre">${logs.map((l) => escapeHtml(l)).join('\n')}</pre>`;
} else {
html += `<p class="run-log-empty">Sem log detalhado (backup pode ter sido criado antes desta versão).</p>`;
}
html += `</div>`;
}
content.innerHTML = html;
} catch (err) {
content.innerHTML = `<p class="changelog-loading">Erro ao carregar log: ${escapeHtml(err.message)}</p>`;
}
}
// Apply translations early (before auth check so login page is translated) // Apply translations early (before auth check so login page is translated)
applyTranslations(); applyTranslations();
checkAuthAndInit(); checkAuthAndInit();

View File

@ -193,10 +193,11 @@
<th>Files</th> <th>Files</th>
<th>Size</th> <th>Size</th>
<th>Started</th> <th>Started</th>
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="allRunsBody"> <tbody id="allRunsBody">
<tr><td colspan="8" class="empty-row">Nenhum run encontrado.</td></tr> <tr><td colspan="9" class="empty-row">Nenhum run encontrado.</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@ -296,6 +297,7 @@
<h2>DockerBackup</h2> <h2>DockerBackup</h2>
</div> </div>
<p class="about-description" data-i18n="about.description">Aplicação web para backup e restauração de volumes Docker com suporte a snapshots incrementais e restore seletivo.</p> <p class="about-description" data-i18n="about.description">Aplicação web para backup e restauração de volumes Docker com suporte a snapshots incrementais e restore seletivo.</p>
<p class="about-author">Desenvolvido por Alexander Sabino em 2026</p>
<div class="about-version-grid"> <div class="about-version-grid">
<div class="about-version-item"> <div class="about-version-item">
@ -319,7 +321,6 @@
<p class="changelog-loading">Carregando changelog...</p> <p class="changelog-loading">Carregando changelog...</p>
</div> </div>
</div> </div>
<p class="about-author">Desenvolvido por Alexander Sabino em 2026</p>
</div> </div>
</div> </div>
@ -579,6 +580,20 @@
</div> </div>
</div> </div>
<!-- Modal: Log do Backup Run -->
<div id="runLogModal" class="modal hidden" aria-hidden="true">
<div class="modal-backdrop" data-action="close-run-log-modal"></div>
<div class="modal-card modal-card--wide" role="dialog" aria-modal="true" aria-labelledby="runLogTitle">
<div class="modal-header">
<h3 id="runLogTitle">Log do Backup</h3>
<button id="runLogModalClose" class="btn btn--ghost btn--sm" type="button">Fechar</button>
</div>
<div id="runLogContent" class="run-log-content">
<p class="changelog-loading">Carregando log...</p>
</div>
</div>
</div>
<script src="/translations.js"></script> <script src="/translations.js"></script>
<script src="/app.js"></script> <script src="/app.js"></script>
</body> </body>

View File

@ -532,6 +532,9 @@ button, input, select {
} }
.form-field input[type='text'], .form-field input[type='text'],
.form-field input[type='password'],
.form-field input[type='datetime-local'],
.form-field select,
.form-fieldset input[type='text'] { .form-fieldset input[type='text'] {
width: 100%; width: 100%;
padding: 9px 12px; padding: 9px 12px;
@ -540,9 +543,25 @@ button, input, select {
background: var(--surface); background: var(--surface);
color: var(--text); color: var(--text);
transition: border-color 150ms; transition: border-color 150ms;
font-size: 14px;
font-family: inherit;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23718096' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
padding-right: 32px;
} }
.form-field input:focus { .form-field input[type='text'],
.form-field input[type='password'],
.form-field input[type='datetime-local'] {
background-image: none;
padding-right: 12px;
}
.form-field input:focus,
.form-field select:focus {
outline: none; outline: none;
border-color: var(--accent); border-color: var(--accent);
} }
@ -1064,15 +1083,23 @@ button, input, select {
} }
.settings-select { .settings-select {
padding: 8px 12px; padding: 8px 12px;
padding-right: 32px;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
font-size: 0.875rem; font-size: 0.875rem;
font-family: inherit;
background: var(--surface); background: var(--surface);
color: var(--text); color: var(--text);
cursor: pointer; cursor: pointer;
max-width: 280px; max-width: 280px;
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23718096' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
transition: border-color 150ms;
} }
.settings-select:focus { outline: 2px solid var(--accent); } .settings-select:focus { outline: none; border-color: var(--accent); }
.toggle-label { .toggle-label {
display: flex; display: flex;
align-items: center; align-items: center;
@ -1210,6 +1237,59 @@ button, input, select {
text-align: center; text-align: center;
} }
/* --- Run Log Modal --------------------------------------- */
.run-log-content {
max-height: 65vh;
overflow-y: auto;
padding: 4px 0;
display: flex;
flex-direction: column;
gap: 16px;
}
.run-log-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.run-log-container-name {
font-size: 0.9rem;
font-weight: 600;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
}
.run-log-error {
background: #fff5f5;
border: 1px solid #feb2b2;
border-radius: var(--radius-sm);
padding: 8px 12px;
font-size: 0.82rem;
color: var(--danger);
}
[data-theme="dark"] .run-log-error {
background: rgba(229, 62, 62, 0.12);
border-color: rgba(229, 62, 62, 0.3);
}
.run-log-pre {
background: #1a1a2e;
color: #c3d3e8;
border-radius: var(--radius-sm);
padding: 12px 14px;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.78rem;
line-height: 1.6;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
margin: 0;
}
.run-log-empty {
font-size: 0.82rem;
color: var(--text-muted);
font-style: italic;
}
/* --- Modal ----------------------------------------------- */ /* --- Modal ----------------------------------------------- */
.modal { .modal {
position: fixed; position: fixed;

View File

@ -516,6 +516,7 @@ class BackupService {
containerBackup.fileCount = Math.max(fileCurrent, fileTotal); containerBackup.fileCount = Math.max(fileCurrent, fileTotal);
try { containerBackup.archiveSize = (await fs.stat(absoluteArchivePath)).size; } catch {} try { containerBackup.archiveSize = (await fs.stat(absoluteArchivePath)).size; } catch {}
containerBackup.logs = [...logs];
onProgress({ onProgress({
containerName, containerName,
@ -594,6 +595,7 @@ class BackupService {
containerBackup.fileCount = Math.max(fileCurrent, fileTotal); containerBackup.fileCount = Math.max(fileCurrent, fileTotal);
try { containerBackup.archiveSize = (await fs.stat(absoluteArchivePath)).size; } catch {} try { containerBackup.archiveSize = (await fs.stat(absoluteArchivePath)).size; } catch {}
containerBackup.logs = [...logs];
onProgress({ onProgress({
containerName, containerName,
@ -670,6 +672,7 @@ class BackupService {
const hostArchivePath = path.join(profile.backupDir, ...archiveRelativePath.split('/')); const hostArchivePath = path.join(profile.backupDir, ...archiveRelativePath.split('/'));
containerBackup.archiveSize = (await fs.stat(hostArchivePath)).size; containerBackup.archiveSize = (await fs.stat(hostArchivePath)).size;
} catch {} } catch {}
containerBackup.logs = [...logs];
onProgress({ onProgress({
containerName, containerName,
@ -698,6 +701,7 @@ class BackupService {
...containerBackup, ...containerBackup,
status: 'error', status: 'error',
error: error.message, error: error.message,
logs: [...logs],
}; };
} }
} }

View File

@ -322,6 +322,19 @@ async function main() {
} }
}); });
app.get('/api/backups/:backupId', authMiddleware, async (request, response) => {
try {
const backup = await store.getBackup(request.params.backupId);
if (!backup) {
response.status(404).json({ error: 'Backup nao encontrado.' });
return;
}
response.json(backup);
} catch (error) {
response.status(500).json({ error: error.message });
}
});
app.post('/api/profiles/:profileId/run', authMiddleware, async (request, response) => { app.post('/api/profiles/:profileId/run', authMiddleware, async (request, response) => {
try { try {
const profileId = request.params.profileId; const profileId = request.params.profileId;