diff --git a/README.md b/README.md index 427bc6b..f0d54e1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- + @@ -18,12 +18,20 @@ > ⚠️ **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.6** +Versão atual: **0.0.7** --- ## � Changelog +### [0.0.7] — 2026-05-09 + +#### Adicionado +- **Exclusão em cascata de Storage Location:** ao excluir um local de armazenamento, o sistema busca automaticamente todos os profiles vinculados e seus respectivos backups e os exclui junto. Antes de confirmar, o usuário recebe um aviso detalhado listando os nomes dos profiles afetados e a quantidade de backups que serão removidos. +- **Rota `GET /api/storage-locations/:id/impact`:** nova rota que retorna, sem fazer alterações, quantos profiles e backups serão impactados pela exclusão de um local de armazenamento. + +--- + ### [0.0.6] — 2026-05-09 #### Adicionado diff --git a/package.json b/package.json index e396fa3..5bf7640 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dockerbackup-app", - "version": "0.0.6", + "version": "0.0.7", "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 189d3e2..5d137bc 100644 --- a/public/app.js +++ b/public/app.js @@ -253,11 +253,32 @@ elements.storageLocationsList?.addEventListener('click', async (e) => { const btn = e.target.closest('[data-storage-action="delete"]'); if (!btn) return; const id = btn.dataset.storageId; - if (!window.confirm('Excluir este local de armazenamento?')) return; + + // Fetch impact before confirming + let impact = { profileCount: 0, profileNames: [], backupCount: 0 }; + try { + impact = await api(`/api/storage-locations/${id}/impact`); + } catch { + // Non-fatal; proceed with generic message + } + + let message = 'Excluir este local de armazenamento?'; + if (impact.profileCount > 0) { + const names = impact.profileNames.map((n) => `• ${n}`).join('\n'); + message = + `⚠️ ATENÇÃO: Esta ação também irá excluir permanentemente:\n\n` + + ` ${impact.profileCount} profile(s) de backup:\n${names}\n\n` + + ` ${impact.backupCount} backup(s) registrado(s) desses profiles\n\n` + + `Deseja continuar?`; + } + + if (!window.confirm(message)) return; try { await api(`/api/storage-locations/${id}`, { method: 'DELETE' }); - await loadStorageLocations(); - showToast('Local removido.'); + await Promise.all([loadStorageLocations(), loadProfiles()]); + showToast(impact.profileCount > 0 + ? `Local removido junto com ${impact.profileCount} profile(s) e ${impact.backupCount} backup(s).` + : 'Local de armazenamento removido.'); } catch (error) { showToast(error.message, true); } diff --git a/src/server.js b/src/server.js index fd9e182..5185986 100644 --- a/src/server.js +++ b/src/server.js @@ -470,6 +470,19 @@ async function main() { } }); + app.get('/api/storage-locations/:id/impact', authMiddleware, async (request, response) => { + try { + const impact = await store.storageLocationImpact(request.params.id); + response.json({ + profileCount: impact.profiles.length, + profileNames: impact.profiles.map((p) => p.name), + backupCount: impact.backupCount, + }); + } catch (error) { + response.status(500).json({ error: error.message }); + } + }); + app.delete('/api/storage-locations/:id', authMiddleware, async (request, response) => { try { await store.deleteStorageLocation(request.params.id); diff --git a/src/store.js b/src/store.js index 2bd8fc0..480ec57 100644 --- a/src/store.js +++ b/src/store.js @@ -58,6 +58,7 @@ class JsonStore { backupScope: profileInput.backupScope || 'volumes', volumeSelections: profileInput.volumeSelections || {}, backupDir: profileInput.backupDir, + storageLocationId: profileInput.storageLocationId || null, updatedAt: now, createdAt: profileInput.createdAt || now, }; @@ -166,8 +167,21 @@ class JsonStore { return location; } + async storageLocationImpact(locationId) { + const data = await this.read(); + const profiles = data.profiles.filter((p) => p.storageLocationId === locationId); + const profileIds = new Set(profiles.map((p) => p.id)); + const backupCount = data.backups.filter((b) => profileIds.has(b.profileId)).length; + return { profiles, backupCount }; + } + async deleteStorageLocation(locationId) { await this.write((data) => { + const profileIds = new Set( + data.profiles.filter((p) => p.storageLocationId === locationId).map((p) => p.id) + ); + data.backups = data.backups.filter((b) => !profileIds.has(b.profileId)); + data.profiles = data.profiles.filter((p) => !profileIds.has(p.id)); data.storageLocations = data.storageLocations.filter((item) => item.id !== locationId); return data; });