atualiza versão para 0.0.6; adiciona suporte a temas personalizados e changelog dinâmico na aba Sobre

This commit is contained in:
Alexander Sabino 2026-05-09 16:43:48 +01:00
parent 0fa741c2bf
commit 2479154f63
5 changed files with 308 additions and 32 deletions

View File

@ -9,7 +9,7 @@
</p> </p>
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/VERSION-0.0.5-blue?style=flat-square" /> <img src="https://img.shields.io/badge/VERSION-0.0.6-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" />
@ -18,12 +18,24 @@
> ⚠️ **AVISO CRÍTICO:** Aplicação em estágio inicial de desenvolvimento. Não use em produção — há risco de perda de dados. > ⚠️ **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.0.5** Versão atual: **0.0.6**
--- ---
## <20> Changelog ## <20> Changelog
### [0.0.6] — 2026-05-09
#### Adicionado
- **Seletor de temas:** nova seção na aba Configurações com 11 temas visuais (Padrão, Escuro, Amanhecer, Floresta, Oceano, Púrpura, Rosa, Laranja, Grafite, Safira, Alto Contraste). O tema selecionado é aplicado imediatamente e salvo no navegador.
- **Changelog dinâmico:** a aba Sobre agora busca e exibe o changelog diretamente do `README.md` do GitHub, sem necessidade de atualização manual na interface.
#### Corrigido
- **Botões Run/Editar/Excluir:** estilizados usando o sistema de design existente (`.btn`). Run ficou azul/primário, Editar em cinza/secundário e Excluir em vermelho com borda.
- **`docker-compose.yml`:** `restart: unless-stopped` descomentado para garantir que o container reinicie automaticamente após uma atualização via botão da aba Sobre.
---
### [0.0.5] — 2026-05-09 ### [0.0.5] — 2026-05-09
#### Adicionado #### Adicionado

View File

@ -1,6 +1,6 @@
{ {
"name": "dockerbackup-app", "name": "dockerbackup-app",
"version": "0.0.5", "version": "0.0.6",
"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

@ -16,6 +16,30 @@ function applyTranslations() {
}); });
} }
// ─── Themes ───────────────────────────────────────────────
const VALID_THEMES = new Set([
'default','dark','sunrise','forest','ocean','purple','rose','orange','graphite','sapphire','contrast'
]);
function applyTheme(theme) {
const safe = VALID_THEMES.has(theme) ? theme : 'default';
if (safe === 'default') {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', safe);
}
localStorage.setItem('theme', safe);
const sel = document.querySelector('#settingsTheme');
if (sel) sel.value = safe;
}
// Apply saved theme immediately
applyTheme(localStorage.getItem('theme') || 'default');
document.addEventListener('change', (e) => {
if (e.target.id === 'settingsTheme') applyTheme(e.target.value);
});
// ─── Auth ────────────────────────────────────────────────── // ─── Auth ──────────────────────────────────────────────────
let authToken = localStorage.getItem('authToken') || null; let authToken = localStorage.getItem('authToken') || null;
@ -688,9 +712,9 @@ function backupButtons(profile) {
</select> </select>
</label> </label>
<div class="card-actions"> <div class="card-actions">
<button data-action="run" data-profile-id="${escapeHtml(profile.id)}" class="primary-button small" ${isRunning ? 'disabled' : ''}>${isRunning ? 'Executando...' : 'Run'}</button> <button data-action="run" data-profile-id="${escapeHtml(profile.id)}" class="btn btn--primary btn--sm" ${isRunning ? 'disabled' : ''}>${isRunning ? 'Executando...' : 'Run'}</button>
<button data-action="edit" data-profile-id="${escapeHtml(profile.id)}" class="secondary-button small">Editar</button> <button data-action="edit" data-profile-id="${escapeHtml(profile.id)}" class="btn btn--secondary btn--sm">Editar</button>
<button data-action="delete" data-profile-id="${escapeHtml(profile.id)}" class="ghost-button small">Excluir</button> <button data-action="delete" data-profile-id="${escapeHtml(profile.id)}" class="btn btn--danger btn--sm">Excluir</button>
</div> </div>
</div> </div>
`; `;
@ -1431,6 +1455,8 @@ function buildLanguageSelect() {
async function loadSettingsView() { async function loadSettingsView() {
buildLanguageSelect(); buildLanguageSelect();
const themeSelect = document.querySelector('#settingsTheme');
if (themeSelect) themeSelect.value = localStorage.getItem('theme') || 'default';
try { try {
const settings = await api('/api/settings'); const settings = await api('/api/settings');
const select = document.querySelector('#settingsLanguage'); const select = document.querySelector('#settingsLanguage');
@ -1471,17 +1497,66 @@ document.querySelector('#saveSettingsBtn')?.addEventListener('click', async () =
}); });
// ─── About ──────────────────────────────────────────────── // ─── About ────────────────────────────────────────────────
function markdownInline(raw) {
return raw
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/`([^`]+)`/g, '<code>$1</code>');
}
function parseChangelogSection(markdown) {
// Extract from ## Changelog (or ## 🗂 Changelog etc.) to next ##
const start = markdown.search(/^##\s+[^\n]*[Cc]hangelog/m);
if (start === -1) return null;
const rest = markdown.slice(start);
const nextSection = rest.slice(1).search(/^## /m);
const section = nextSection === -1 ? rest : rest.slice(0, nextSection + 1);
const lines = section.split('\n');
let html = '';
let inList = false;
for (const line of lines) {
const h3 = line.match(/^###\s+(.+)/);
const h4 = line.match(/^####\s+(.+)/);
const li = line.match(/^-\s+(.+)/);
if (h3) {
if (inList) { html += '</ul>'; inList = false; }
html += `<h4>${markdownInline(h3[1].replace(/\[([^\]]+)\]/, '$1'))}</h4>`;
} else if (h4) {
if (inList) { html += '</ul>'; inList = false; }
html += `<h5 class="changelog-section">${markdownInline(h4[1])}</h5>`;
} else if (li) {
if (!inList) { html += '<ul>'; inList = true; }
html += `<li>${markdownInline(li[1])}</li>`;
} else if (line.startsWith('---') || line.startsWith('## ')) {
if (inList) { html += '</ul>'; inList = false; }
}
}
if (inList) html += '</ul>';
return html || null;
}
async function loadAboutView() { async function loadAboutView() {
const currentVerEl = document.querySelector('#aboutCurrentVersion'); const currentVerEl = document.querySelector('#aboutCurrentVersion');
const latestVerEl = document.querySelector('#aboutLatestVersion'); const latestVerEl = document.querySelector('#aboutLatestVersion');
const updateWrap = document.querySelector('#aboutUpdateWrap'); const updateWrap = document.querySelector('#aboutUpdateWrap');
const updateStatus = document.querySelector('#aboutUpdateStatus'); const updateStatus = document.querySelector('#aboutUpdateStatus');
const updateBtn = document.querySelector('#aboutUpdateBtn'); const updateBtn = document.querySelector('#aboutUpdateBtn');
const changelogEl = document.querySelector('#aboutChangelog');
if (latestVerEl) latestVerEl.textContent = t('about.checking'); if (latestVerEl) latestVerEl.textContent = t('about.checking');
try { // Fetch version info and changelog in parallel
const about = await api('/api/about'); const [aboutResult, changelogResult] = await Promise.allSettled([
api('/api/about'),
fetch('https://raw.githubusercontent.com/asabino2/dockerbackup/main/README.md').then((r) => r.text()),
]);
// Version info
if (aboutResult.status === 'fulfilled') {
const about = aboutResult.value;
const current = about.currentVersion || '—'; const current = about.currentVersion || '—';
const latest = about.latestVersion || null; const latest = about.latestVersion || null;
@ -1499,10 +1574,20 @@ async function loadAboutView() {
if (updateStatus) updateStatus.textContent = t('about.checkError'); if (updateStatus) updateStatus.textContent = t('about.checkError');
if (updateBtn) updateBtn.classList.add('hidden'); if (updateBtn) updateBtn.classList.add('hidden');
} }
} catch (error) { } else {
if (currentVerEl) currentVerEl.textContent = '—'; if (currentVerEl) currentVerEl.textContent = '—';
if (latestVerEl) latestVerEl.textContent = '—'; if (latestVerEl) latestVerEl.textContent = '—';
showToast(error.message, true); showToast(aboutResult.reason?.message || 'Erro ao buscar versão', true);
}
// Changelog from GitHub README
if (changelogEl) {
if (changelogResult.status === 'fulfilled') {
const html = parseChangelogSection(changelogResult.value);
changelogEl.innerHTML = html || '<p class="changelog-loading">Changelog não encontrado.</p>';
} else {
changelogEl.innerHTML = '<p class="changelog-loading">Não foi possível carregar o changelog.</p>';
}
} }
} }

View File

@ -228,6 +228,23 @@
<select id="settingsLanguage" class="settings-select"> <select id="settingsLanguage" class="settings-select">
</select> </select>
</div> </div>
<div class="settings-section">
<h2>Tema</h2>
<p>Personalize a aparência da interface</p>
<select id="settingsTheme" class="settings-select">
<option value="default">Padrão (Azul)</option>
<option value="dark">Escuro</option>
<option value="sunrise">Amanhecer</option>
<option value="forest">Floresta</option>
<option value="ocean">Oceano</option>
<option value="purple">Púrpura</option>
<option value="rose">Rosa</option>
<option value="orange">Laranja</option>
<option value="graphite">Grafite</option>
<option value="sapphire">Safira</option>
<option value="contrast">Alto Contraste</option>
</select>
</div>
<div class="settings-section"> <div class="settings-section">
<h2 data-i18n="settings.auth">Controle de Acesso</h2> <h2 data-i18n="settings.auth">Controle de Acesso</h2>
<label class="toggle-label"> <label class="toggle-label">
@ -282,28 +299,7 @@
<div class="about-changelog"> <div class="about-changelog">
<h3 data-i18n="about.changelog">Últimas alterações</h3> <h3 data-i18n="about.changelog">Últimas alterações</h3>
<div id="aboutChangelog" class="changelog-content"> <div id="aboutChangelog" class="changelog-content">
<h4>0.0.4</h4> <p class="changelog-loading">Carregando changelog...</p>
<ul>
<li>Correção: contador de arquivos no progresso do backup ultrapassava o total</li>
<li>Correção: aba Sobre não exibia a última versão disponível</li>
</ul>
<h4>0.0.3</h4>
<ul>
<li>Suporte a múltiplos idiomas (10 idiomas)</li>
<li>Aba de configurações com controle de acesso (usuário e senha)</li>
<li>Aba "Sobre" com verificação de versão via GitHub e atualização automática</li>
</ul>
<h4>0.0.2</h4>
<ul>
<li>Adicionado gerenciamento de Storage Locations</li>
<li>Backup incremental com seleção de full backup base</li>
<li>Agrupamento visual de backups incrementais sob o full</li>
<li>Removidas abas Servers e Naming Rules</li>
</ul>
<h4>0.0.1</h4>
<ul>
<li>Versão inicial: backup e restore de volumes Docker</li>
</ul>
</div> </div>
</div> </div>
</div> </div>

View File

@ -27,6 +27,168 @@
--shadow-md: 0 4px 12px rgba(0,0,0,0.08); --shadow-md: 0 4px 12px rgba(0,0,0,0.08);
} }
/* --- Themes --------------------------------------------- */
[data-theme="dark"] {
--sidebar-bg: #0a0a14;
--sidebar-text: #8892a4;
--sidebar-text-active: #e2e8f0;
--sidebar-active-bg: rgba(255,255,255,0.1);
--sidebar-hover-bg: rgba(255,255,255,0.06);
--sidebar-accent: #60a5fa;
--bg: #0f172a;
--surface: #1e293b;
--border: #334155;
--text: #e2e8f0;
--text-muted: #94a3b8;
--text-sm: #cbd5e1;
--accent: #60a5fa;
--accent-hover: #3b82f6;
}
[data-theme="sunrise"] {
--sidebar-bg: #7c2d12;
--sidebar-text: #fed7aa;
--sidebar-text-active: #fff7ed;
--sidebar-active-bg: rgba(255,255,255,0.12);
--sidebar-hover-bg: rgba(255,255,255,0.07);
--sidebar-accent: #fb923c;
--bg: #fff7ed;
--surface: #ffffff;
--border: #fed7aa;
--text: #431407;
--text-muted: #9a3412;
--text-sm: #7c2d12;
--accent: #ea580c;
--accent-hover: #c2410c;
}
[data-theme="forest"] {
--sidebar-bg: #14532d;
--sidebar-text: #bbf7d0;
--sidebar-text-active: #f0fdf4;
--sidebar-active-bg: rgba(255,255,255,0.1);
--sidebar-hover-bg: rgba(255,255,255,0.06);
--sidebar-accent: #4ade80;
--bg: #f0fdf4;
--surface: #ffffff;
--border: #bbf7d0;
--text: #14532d;
--text-muted: #166534;
--text-sm: #15803d;
--accent: #16a34a;
--accent-hover: #15803d;
}
[data-theme="ocean"] {
--sidebar-bg: #164e63;
--sidebar-text: #a5f3fc;
--sidebar-text-active: #ecfeff;
--sidebar-active-bg: rgba(255,255,255,0.1);
--sidebar-hover-bg: rgba(255,255,255,0.06);
--sidebar-accent: #22d3ee;
--bg: #ecfeff;
--surface: #ffffff;
--border: #a5f3fc;
--text: #164e63;
--text-muted: #0e7490;
--text-sm: #0891b2;
--accent: #0891b2;
--accent-hover: #0e7490;
}
[data-theme="purple"] {
--sidebar-bg: #3b0764;
--sidebar-text: #e9d5ff;
--sidebar-text-active: #faf5ff;
--sidebar-active-bg: rgba(255,255,255,0.1);
--sidebar-hover-bg: rgba(255,255,255,0.06);
--sidebar-accent: #c084fc;
--bg: #faf5ff;
--surface: #ffffff;
--border: #e9d5ff;
--text: #3b0764;
--text-muted: #7e22ce;
--text-sm: #6b21a8;
--accent: #9333ea;
--accent-hover: #7e22ce;
}
[data-theme="rose"] {
--sidebar-bg: #881337;
--sidebar-text: #fecdd3;
--sidebar-text-active: #fff1f2;
--sidebar-active-bg: rgba(255,255,255,0.1);
--sidebar-hover-bg: rgba(255,255,255,0.06);
--sidebar-accent: #fb7185;
--bg: #fff1f2;
--surface: #ffffff;
--border: #fecdd3;
--text: #881337;
--text-muted: #be123c;
--text-sm: #9f1239;
--accent: #e11d48;
--accent-hover: #be123c;
}
[data-theme="orange"] {
--sidebar-bg: #431407;
--sidebar-text: #fed7aa;
--sidebar-text-active: #fff7ed;
--sidebar-active-bg: rgba(255,255,255,0.1);
--sidebar-hover-bg: rgba(255,255,255,0.06);
--sidebar-accent: #fb923c;
--bg: #fff7ed;
--surface: #ffffff;
--border: #fed7aa;
--text: #1c1917;
--text-muted: #78350f;
--text-sm: #92400e;
--accent: #f97316;
--accent-hover: #ea580c;
}
[data-theme="graphite"] {
--sidebar-bg: #111827;
--sidebar-text: #9ca3af;
--sidebar-text-active: #f9fafb;
--sidebar-active-bg: rgba(255,255,255,0.08);
--sidebar-hover-bg: rgba(255,255,255,0.04);
--sidebar-accent: #9ca3af;
--bg: #f3f4f6;
--surface: #ffffff;
--border: #d1d5db;
--text: #111827;
--text-muted: #6b7280;
--text-sm: #374151;
--accent: #374151;
--accent-hover: #1f2937;
}
[data-theme="sapphire"] {
--sidebar-bg: #1e1b4b;
--sidebar-text: #c7d2fe;
--sidebar-text-active: #eef2ff;
--sidebar-active-bg: rgba(255,255,255,0.1);
--sidebar-hover-bg: rgba(255,255,255,0.06);
--sidebar-accent: #818cf8;
--bg: #eef2ff;
--surface: #ffffff;
--border: #c7d2fe;
--text: #1e1b4b;
--text-muted: #4338ca;
--text-sm: #3730a3;
--accent: #4f46e5;
--accent-hover: #4338ca;
}
[data-theme="contrast"] {
--sidebar-bg: #000000;
--sidebar-text: #e5e5e5;
--sidebar-text-active: #ffff00;
--sidebar-active-bg: rgba(255,255,0,0.15);
--sidebar-hover-bg: rgba(255,255,255,0.08);
--sidebar-accent: #ffff00;
--bg: #ffffff;
--surface: #ffffff;
--border: #000000;
--text: #000000;
--text-muted: #333333;
--text-sm: #1a1a1a;
--accent: #0000cc;
--accent-hover: #000099;
}
body { body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
font-size: 14px; font-size: 14px;
@ -279,9 +441,17 @@ button, input, select {
} }
.btn--ghost { .btn--ghost {
background: transparent;
color: var(--text-muted);
border: 1px solid var(--border);
}
.btn--danger {
background: #fff0f0; background: #fff0f0;
color: var(--danger); color: var(--danger);
border: 1px solid #fecaca;
} }
.btn--danger:not(:disabled):hover { background: #fee2e2; opacity: 1; }
.btn--sm { padding: 5px 11px; font-size: 12.5px; } .btn--sm { padding: 5px 11px; font-size: 12.5px; }
@ -1020,6 +1190,19 @@ button, input, select {
font-size: 0.82rem; font-size: 0.82rem;
color: var(--text-muted); color: var(--text-muted);
} }
.changelog-content h5.changelog-section {
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin: 8px 0 4px;
}
.changelog-loading {
font-size: 0.82rem;
color: var(--text-muted);
font-style: italic;
}
/* --- Modal ----------------------------------------------- */ /* --- Modal ----------------------------------------------- */
.modal { .modal {