From 2eed853115b71ec082a58bcc3da7ea2471eef121 Mon Sep 17 00:00:00 2001
From: Alexander Sabino <32822107+asabino2@users.noreply.github.com>
Date: Thu, 7 May 2026 16:34:27 +0100
Subject: [PATCH] Refactor styles for improved layout and responsiveness;
update color scheme and component styles for a modern look
---
public/app.js | 162 ++++++++
public/index.html | 346 ++++++++++++----
public/styles.css | 989 ++++++++++++++++++++++++++++++----------------
3 files changed, 1082 insertions(+), 415 deletions(-)
diff --git a/public/app.js b/public/app.js
index db0aab6..54b61bb 100644
--- a/public/app.js
+++ b/public/app.js
@@ -30,6 +30,167 @@ const elements = {
volumePickerSelectAll: document.querySelector('#volumePickerSelectAll'),
};
+// ─── View navigation ──────────────────────────────────────
+function navigateTo(viewName) {
+ for (const view of document.querySelectorAll('.view')) {
+ view.classList.add('hidden');
+ }
+ const target = document.querySelector(`#view-${viewName}`);
+ if (target) {
+ target.classList.remove('hidden');
+ }
+ for (const item of document.querySelectorAll('.nav-item')) {
+ item.classList.toggle('active', item.dataset.view === viewName);
+ }
+ if (viewName === 'profiles') {
+ loadProfiles();
+ loadContainers();
+ }
+ if (viewName === 'servers') {
+ renderServers();
+ }
+ if (viewName === 'backups') {
+ renderBackupsView();
+ }
+}
+
+document.querySelector('.sidebar').addEventListener('click', (e) => {
+ const btn = e.target.closest('.nav-item[data-view]');
+ if (btn) {
+ navigateTo(btn.dataset.view);
+ }
+});
+
+document.querySelector('#createProfileBtn')?.addEventListener('click', () => navigateTo('profiles'));
+document.querySelector('#refreshServers')?.addEventListener('click', () => renderServers());
+
+function renderServers() {
+ const list = document.querySelector('#serversList');
+ if (!list) return;
+ if (!state.containers.length) {
+ list.innerHTML = '
Nenhum servidor encontrado.
';
+ return;
+ }
+ list.innerHTML = state.containers.map((c) => `
+
+
${escapeHtml(c.name)}
+ ${escapeHtml(c.image)}
+ ${escapeHtml(c.status)} · ${escapeHtml(c.state)}
+
+ `).join('');
+}
+
+async function renderBackupsView() {
+ const host = document.querySelector('#backupsViewList');
+ if (!host) return;
+ if (!state.profiles.length) {
+ host.innerHTML = 'Nenhum profile encontrado.
';
+ return;
+ }
+ const rows = await Promise.all(state.profiles.map(async (p) => {
+ const backups = await api(`/api/profiles/${p.id}/backups`);
+ return { profile: p, backups };
+ }));
+ host.innerHTML = rows.map(({ profile, backups }) => `
+
+
+
${escapeHtml(profile.name)}
+ ${escapeHtml(String(backups.length))} backup(s)
+
+
+
+ | Data | Mode | Status | Containers |
+
+ ${backups.length ? backups.map((b) => `
+
+ | ${escapeHtml(new Date(b.createdAt).toLocaleString('pt-BR'))} |
+ ${escapeHtml(b.mode || '—')} |
+ ${escapeHtml(b.status)} |
+ ${escapeHtml((b.containers || []).map((c) => c.containerName).join(', '))} |
+
+ `).join('') : '| Nenhum backup realizado. |
'}
+
+
+
+
+ `).join('');
+}
+
+async function updateDashboard() {
+ const allBackups = (await Promise.all(
+ state.profiles.map((p) => api(`/api/profiles/${p.id}/backups`))
+ )).flat();
+
+ // Last successful
+ const successful = allBackups
+ .filter((b) => b.status === 'ok' || b.status === 'partial')
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
+ const lastBackupEl = document.querySelector('#lastBackupTime');
+ if (lastBackupEl) {
+ lastBackupEl.textContent = successful.length
+ ? new Date(successful[0].createdAt).toLocaleString('pt-BR')
+ : '—';
+ }
+
+ // Failed in last 24h
+ const cutoff = Date.now() - 24 * 60 * 60 * 1000;
+ const failed = allBackups.filter(
+ (b) => b.status === 'error' && new Date(b.createdAt).getTime() >= cutoff,
+ );
+ const failedEl = document.querySelector('#failedCount');
+ if (failedEl) failedEl.textContent = String(failed.length);
+
+ // Recent runs table
+ const recent = [...allBackups]
+ .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
+ .slice(0, 20);
+
+ const tbody = document.querySelector('#recentRunsBody');
+ if (!tbody) return;
+
+ if (!recent.length) {
+ tbody.innerHTML = '| Nenhum run encontrado. |
';
+ return;
+ }
+
+ tbody.innerHTML = recent.map((b, index) => {
+ const profile = state.profiles.find((p) => p.id === b.profileId);
+ const profileName = profile ? profile.name : (b.profileName || b.profileId || '—');
+ const started = new Date(b.createdAt).toLocaleString('pt-BR');
+ const duration = '—';
+ const fileCount = (b.containers || []).reduce((sum, c) => sum + (c.fileCount || 0), 0);
+ const size = (b.containers || []).reduce((sum, c) => sum + (c.archiveSize || 0), 0);
+ const sizeStr = size > 0 ? formatBytes(size) : '—';
+ return `
+
+ | #${index + 1} |
+ ${escapeHtml(profileName)} |
+ ${escapeHtml(b.status === 'ok' ? 'Completed' : b.status)} |
+ ${fileCount || '—'} |
+ ${sizeStr} |
+ ${started} |
+ ${duration} |
+ |
+
+ `;
+ }).join('');
+
+ document.querySelector('#recentRunsBody')?.addEventListener('click', (e) => {
+ const link = e.target.closest('.profile-link');
+ if (link) {
+ e.preventDefault();
+ navigateTo('profiles');
+ }
+ }, { once: true });
+}
+
+function formatBytes(bytes) {
+ if (bytes >= 1e9) return (bytes / 1e9).toFixed(1) + ' GB';
+ if (bytes >= 1e6) return (bytes / 1e6).toFixed(1) + ' MB';
+ if (bytes >= 1e3) return (bytes / 1e3).toFixed(1) + ' KB';
+ return bytes + ' B';
+}
+
async function api(path, options = {}) {
const response = await fetch(path, {
headers: {
@@ -752,6 +913,7 @@ async function handleProfileAction(event) {
async function init() {
try {
await Promise.all([loadContainers(), loadProfiles()]);
+ await updateDashboard();
} catch (error) {
showToast(error.message, true);
}
diff --git a/public/index.html b/public/index.html
index 7ecb5d5..a26c1dd 100644
--- a/public/index.html
+++ b/public/index.html
@@ -3,117 +3,311 @@
- Docker Backup Profiles
+ DockerBackup
-
+
-