adiciona modal para criação e edição de profiles; atualiza estilos e lógica de exibição

This commit is contained in:
Alexander Sabino 2026-05-08 19:27:19 +01:00
parent ef9ffdbe71
commit 11c58f0cfe
4 changed files with 112 additions and 62 deletions

View File

@ -14,8 +14,9 @@ const elements = {
backupDir: document.querySelector('#backupDir'), backupDir: document.querySelector('#backupDir'),
containerOptions: document.querySelector('#containerOptions'), containerOptions: document.querySelector('#containerOptions'),
profilesList: document.querySelector('#profilesList'), profilesList: document.querySelector('#profilesList'),
formModeBadge: document.querySelector('#formModeBadge'),
toast: document.querySelector('#toast'), toast: document.querySelector('#toast'),
profileFormModal: document.querySelector('#profileFormModal'),
profileModalClose: document.querySelector('#profileModalClose'),
restoreModal: document.querySelector('#restoreModal'), restoreModal: document.querySelector('#restoreModal'),
restoreModalSubtitle: document.querySelector('#restoreModalSubtitle'), restoreModalSubtitle: document.querySelector('#restoreModalSubtitle'),
restoreContainerOptions: document.querySelector('#restoreContainerOptions'), restoreContainerOptions: document.querySelector('#restoreContainerOptions'),
@ -67,6 +68,31 @@ document.querySelector('.sidebar').addEventListener('click', (e) => {
document.querySelector('#createProfileBtn')?.addEventListener('click', () => navigateTo('profiles')); document.querySelector('#createProfileBtn')?.addEventListener('click', () => navigateTo('profiles'));
document.querySelector('#refreshServers')?.addEventListener('click', () => renderServers()); document.querySelector('#refreshServers')?.addEventListener('click', () => renderServers());
// ─── Profile form modal ───────────────────────────────────
function openProfileModal(title = 'Novo Profile') {
document.querySelector('#profileModalTitle').textContent = title;
elements.profileFormModal.classList.remove('hidden');
elements.profileFormModal.setAttribute('aria-hidden', 'false');
}
function closeProfileModal() {
elements.profileFormModal.classList.add('hidden');
elements.profileFormModal.setAttribute('aria-hidden', 'true');
}
document.querySelector('#openCreateProfileModal')?.addEventListener('click', () => {
resetForm();
openProfileModal('Novo Profile');
});
elements.profileModalClose?.addEventListener('click', closeProfileModal);
elements.profileFormModal?.addEventListener('click', (e) => {
if (e.target.closest('[data-action="close-profile-modal"]')) {
closeProfileModal();
}
});
function renderServers() { function renderServers() {
const list = document.querySelector('#serversList'); const list = document.querySelector('#serversList');
if (!list) return; if (!list) return;
@ -754,9 +780,9 @@ async function renderProfiles() {
function resetForm() { function resetForm() {
elements.profileForm.reset(); elements.profileForm.reset();
elements.profileId.value = ''; elements.profileId.value = '';
elements.formModeBadge.textContent = 'criar';
state.volumeSelections = {}; state.volumeSelections = {};
renderContainers(); renderContainers();
closeProfileModal();
} }
function fillForm(profile) { function fillForm(profile) {
@ -765,7 +791,6 @@ function fillForm(profile) {
elements.backupDir.value = profile.backupDir; elements.backupDir.value = profile.backupDir;
const backupScope = profile.backupScope === 'container' ? 'container' : 'volumes'; const backupScope = profile.backupScope === 'container' ? 'container' : 'volumes';
document.querySelector(`input[name="backupScope"][value="${backupScope}"]`).checked = true; document.querySelector(`input[name="backupScope"][value="${backupScope}"]`).checked = true;
elements.formModeBadge.textContent = 'editar';
state.volumeSelections = Object.assign({}, profile.volumeSelections || {}); state.volumeSelections = Object.assign({}, profile.volumeSelections || {});
renderContainers(); renderContainers();
for (const containerId of profile.containerIds) { for (const containerId of profile.containerIds) {
@ -774,7 +799,7 @@ function fillForm(profile) {
input.checked = true; input.checked = true;
} }
} }
window.scrollTo({ top: 0, behavior: 'smooth' }); openProfileModal('Editar Profile');
} }
async function loadContainers() { async function loadContainers() {
@ -815,6 +840,7 @@ async function saveProfile(event) {
method: 'POST', method: 'POST',
body: JSON.stringify(payload), body: JSON.stringify(payload),
}); });
closeProfileModal();
await loadProfiles(); await loadProfiles();
resetForm(); resetForm();
showToast('Profile salvo.'); showToast('Profile salvo.');

View File

@ -167,68 +167,16 @@
<div id="view-profiles" class="view hidden"> <div id="view-profiles" class="view hidden">
<div class="page-header"> <div class="page-header">
<h1 class="page-title">Backup Profiles</h1> <h1 class="page-title">Backup Profiles</h1>
<div class="page-header-actions">
<button id="openCreateProfileModal" class="btn btn--primary">+ Criar Profile</button>
<button id="reloadProfiles" class="btn btn--secondary">Reload</button> <button id="reloadProfiles" class="btn btn--secondary">Reload</button>
</div> </div>
<div class="profiles-layout">
<div class="card">
<div class="card-toolbar">
<h2 class="card-title">New Profile</h2>
<span id="formModeBadge" class="badge badge--accent">criar</span>
</div>
<form id="profileForm" class="profile-form">
<input type="hidden" id="profileId" />
<div class="form-field">
<label for="profileName">Nome do profile</label>
<input id="profileName" name="name" type="text" placeholder="Backup banco principal" required />
</div>
<div class="form-field">
<label for="backupDir">Diretório de backup no host Docker</label>
<input id="backupDir" name="backupDir" type="text" placeholder="/srv/docker-backups" required />
</div>
<fieldset class="form-fieldset">
<legend>Escopo do backup</legend>
<div class="radio-grid">
<label class="radio-card">
<input type="radio" name="backupScope" value="volumes" checked />
<span>Somente volumes</span>
<small>mantém o comportamento atual de backup de volumes e binds</small>
</label>
<label class="radio-card">
<input type="radio" name="backupScope" value="container" />
<span>Container inteiro</span>
<small>gera um único tar por container a partir de /</small>
</label>
</div>
</fieldset>
<div class="container-picker">
<div class="picker-header">
<span class="picker-label">Containers</span>
<button id="refreshContainers" type="button" class="btn btn--ghost btn--sm">Atualizar lista</button>
</div>
<div id="containerOptions" class="container-options"></div>
</div>
<div class="form-actions">
<button class="btn btn--primary" type="submit">Salvar profile</button>
<button class="btn btn--secondary" type="button" id="clearForm">Limpar</button>
</div>
</form>
</div> </div>
<div class="card"> <div class="card">
<div class="card-toolbar">
<h2 class="card-title">Profiles Salvos</h2>
</div>
<div id="profilesList" class="profiles-list"></div> <div id="profilesList" class="profiles-list"></div>
</div> </div>
</div> </div>
</div>
<!-- VIEW: Backup Runs --> <!-- VIEW: Backup Runs -->
<div id="view-runs" class="view hidden"> <div id="view-runs" class="view hidden">
@ -318,6 +266,59 @@
</div> </div>
</div> </div>
</div> </div>
<div id="profileFormModal" class="modal hidden" aria-hidden="true">
<div class="modal-backdrop" data-action="close-profile-modal"></div>
<div class="modal-card modal-card--wide" role="dialog" aria-modal="true" aria-labelledby="profileModalTitle">
<div class="modal-header">
<h3 id="profileModalTitle">Novo Profile</h3>
<button id="profileModalClose" class="btn btn--ghost btn--sm" type="button">Fechar</button>
</div>
<form id="profileForm" class="profile-form">
<input type="hidden" id="profileId" />
<div class="form-field">
<label for="profileName">Nome do profile</label>
<input id="profileName" name="name" type="text" placeholder="Backup banco principal" required />
</div>
<div class="form-field">
<label for="backupDir">Diretório de backup no host Docker</label>
<input id="backupDir" name="backupDir" type="text" placeholder="/srv/docker-backups" required />
</div>
<fieldset class="form-fieldset">
<legend>Escopo do backup</legend>
<div class="radio-grid">
<label class="radio-card">
<input type="radio" name="backupScope" value="volumes" checked />
<span>Somente volumes</span>
<small>mantém o comportamento atual de backup de volumes e binds</small>
</label>
<label class="radio-card">
<input type="radio" name="backupScope" value="container" />
<span>Container inteiro</span>
<small>gera um único tar por container a partir de /</small>
</label>
</div>
</fieldset>
<div class="container-picker">
<div class="picker-header">
<span class="picker-label">Containers</span>
<button id="refreshContainers" type="button" class="btn btn--ghost btn--sm">Atualizar lista</button>
</div>
<div id="containerOptions" class="container-options"></div>
</div>
<div class="form-actions">
<button class="btn btn--primary" type="submit">Salvar profile</button>
<button class="btn btn--secondary" type="button" id="clearForm">Cancelar</button>
</div>
</form>
</div>
</div>
<script src="/app.js"></script> <script src="/app.js"></script>
</body> </body>
</html> </html>

View File

@ -146,6 +146,12 @@ button, input, select {
margin-bottom: 24px; margin-bottom: 24px;
} }
.page-header-actions {
display: flex;
gap: 8px;
align-items: center;
}
.page-title { .page-title {
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 700;
@ -806,6 +812,11 @@ button, input, select {
gap: 12px; gap: 12px;
} }
.modal-card--wide {
width: min(720px, calc(100vw - 24px));
max-height: min(90vh, 800px);
}
.modal-header { .modal-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;

View File

@ -510,6 +510,9 @@ class BackupService {
pushLog(`Arquivo gerado via helper: ${absoluteArchivePath}`, 'finalizando'); pushLog(`Arquivo gerado via helper: ${absoluteArchivePath}`, 'finalizando');
pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando'); pushLog('Snapshot incremental salvo no diretorio de backup.', 'finalizando');
containerBackup.fileCount = Math.max(fileCurrent, fileTotal);
try { containerBackup.archiveSize = (await fs.stat(absoluteArchivePath)).size; } catch {}
onProgress({ onProgress({
containerName, containerName,
status: 'ok', status: 'ok',
@ -585,6 +588,9 @@ class BackupService {
} }
} }
containerBackup.fileCount = Math.max(fileCurrent, fileTotal);
try { containerBackup.archiveSize = (await fs.stat(absoluteArchivePath)).size; } catch {}
onProgress({ onProgress({
containerName, containerName,
status: 'ok', status: 'ok',
@ -655,6 +661,12 @@ class BackupService {
await this.dockerService.startContainer(containerId).catch(() => null); await this.dockerService.startContainer(containerId).catch(() => null);
} }
containerBackup.fileCount = Math.max(fileCurrent, fileTotal);
try {
const hostArchivePath = path.join(profile.backupDir, ...archiveRelativePath.split('/'));
containerBackup.archiveSize = (await fs.stat(hostArchivePath)).size;
} catch {}
onProgress({ onProgress({
containerName, containerName,
status: 'ok', status: 'ok',