From 2479154f63752c95820db5f0c4506bbf835a7d4e Mon Sep 17 00:00:00 2001
From: Alexander Sabino <32822107+asabino2@users.noreply.github.com>
Date: Sat, 9 May 2026 16:43:48 +0100
Subject: [PATCH] =?UTF-8?q?atualiza=20vers=C3=A3o=20para=200.0.6;=20adicio?=
=?UTF-8?q?na=20suporte=20a=20temas=20personalizados=20e=20changelog=20din?=
=?UTF-8?q?=C3=A2mico=20na=20aba=20Sobre?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
README.md | 16 +++-
package.json | 2 +-
public/app.js | 99 +++++++++++++++++++++++--
public/index.html | 40 +++++-----
public/styles.css | 183 ++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 308 insertions(+), 32 deletions(-)
diff --git a/README.md b/README.md
index c98bae5..427bc6b 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -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.
-Versão atual: **0.0.5**
+Versão atual: **0.0.6**
---
## � 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
#### Adicionado
diff --git a/package.json b/package.json
index fd00261..e396fa3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "dockerbackup-app",
- "version": "0.0.5",
+ "version": "0.0.6",
"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 d39a174..189d3e2 100644
--- a/public/app.js
+++ b/public/app.js
@@ -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 ──────────────────────────────────────────────────
let authToken = localStorage.getItem('authToken') || null;
@@ -688,9 +712,9 @@ function backupButtons(profile) {
-
-
-
+
+
+
`;
@@ -1431,6 +1455,8 @@ function buildLanguageSelect() {
async function loadSettingsView() {
buildLanguageSelect();
+ const themeSelect = document.querySelector('#settingsTheme');
+ if (themeSelect) themeSelect.value = localStorage.getItem('theme') || 'default';
try {
const settings = await api('/api/settings');
const select = document.querySelector('#settingsLanguage');
@@ -1471,17 +1497,66 @@ document.querySelector('#saveSettingsBtn')?.addEventListener('click', async () =
});
// ─── About ────────────────────────────────────────────────
+function markdownInline(raw) {
+ return raw
+ .replace(/&/g, '&').replace(//g, '>')
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
+ .replace(/`([^`]+)`/g, '$1');
+}
+
+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 += ''; inList = false; }
+ html += `${markdownInline(h3[1].replace(/\[([^\]]+)\]/, '$1'))}
`;
+ } else if (h4) {
+ if (inList) { html += ''; inList = false; }
+ html += `${markdownInline(h4[1])}
`;
+ } else if (li) {
+ if (!inList) { html += ''; inList = true; }
+ html += `- ${markdownInline(li[1])}
`;
+ } else if (line.startsWith('---') || line.startsWith('## ')) {
+ if (inList) { html += '
'; inList = false; }
+ }
+ }
+ if (inList) html += '';
+ return html || null;
+}
+
async function loadAboutView() {
const currentVerEl = document.querySelector('#aboutCurrentVersion');
const latestVerEl = document.querySelector('#aboutLatestVersion');
const updateWrap = document.querySelector('#aboutUpdateWrap');
const updateStatus = document.querySelector('#aboutUpdateStatus');
const updateBtn = document.querySelector('#aboutUpdateBtn');
+ const changelogEl = document.querySelector('#aboutChangelog');
if (latestVerEl) latestVerEl.textContent = t('about.checking');
- try {
- const about = await api('/api/about');
+ // Fetch version info and changelog in parallel
+ 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 latest = about.latestVersion || null;
@@ -1499,10 +1574,20 @@ async function loadAboutView() {
if (updateStatus) updateStatus.textContent = t('about.checkError');
if (updateBtn) updateBtn.classList.add('hidden');
}
- } catch (error) {
+ } else {
if (currentVerEl) currentVerEl.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 || 'Changelog não encontrado.
';
+ } else {
+ changelogEl.innerHTML = 'Não foi possível carregar o changelog.
';
+ }
}
}
diff --git a/public/index.html b/public/index.html
index e08d50e..33eaeb3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -228,6 +228,23 @@
+
+
Tema
+
Personalize a aparência da interface
+
+
Controle de Acesso
diff --git a/public/styles.css b/public/styles.css
index 99237e5..e7c25bc 100644
--- a/public/styles.css
+++ b/public/styles.css
@@ -27,6 +27,168 @@
--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 {
font-family: 'Inter', sans-serif;
font-size: 14px;
@@ -279,9 +441,17 @@ button, input, select {
}
.btn--ghost {
+ background: transparent;
+ color: var(--text-muted);
+ border: 1px solid var(--border);
+}
+
+.btn--danger {
background: #fff0f0;
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; }
@@ -1020,6 +1190,19 @@ button, input, select {
font-size: 0.82rem;
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 {