atualiza versão para 0.0.7; adiciona exclusão em cascata de locais de armazenamento e nova rota para verificar impacto
This commit is contained in:
parent
2479154f63
commit
164fe7ecf1
12
README.md
12
README.md
|
|
@ -9,7 +9,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/VERSION-0.0.6-blue?style=flat-square" />
|
<img src="https://img.shields.io/badge/VERSION-0.0.7-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,20 @@
|
||||||
|
|
||||||
> ⚠️ **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.6**
|
Versão atual: **0.0.7**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## <20> Changelog
|
## <20> 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
|
### [0.0.6] — 2026-05-09
|
||||||
|
|
||||||
#### Adicionado
|
#### Adicionado
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "dockerbackup-app",
|
"name": "dockerbackup-app",
|
||||||
"version": "0.0.6",
|
"version": "0.0.7",
|
||||||
"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": {
|
||||||
|
|
|
||||||
|
|
@ -253,11 +253,32 @@ elements.storageLocationsList?.addEventListener('click', async (e) => {
|
||||||
const btn = e.target.closest('[data-storage-action="delete"]');
|
const btn = e.target.closest('[data-storage-action="delete"]');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
const id = btn.dataset.storageId;
|
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 {
|
try {
|
||||||
await api(`/api/storage-locations/${id}`, { method: 'DELETE' });
|
await api(`/api/storage-locations/${id}`, { method: 'DELETE' });
|
||||||
await loadStorageLocations();
|
await Promise.all([loadStorageLocations(), loadProfiles()]);
|
||||||
showToast('Local removido.');
|
showToast(impact.profileCount > 0
|
||||||
|
? `Local removido junto com ${impact.profileCount} profile(s) e ${impact.backupCount} backup(s).`
|
||||||
|
: 'Local de armazenamento removido.');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(error.message, true);
|
showToast(error.message, true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
app.delete('/api/storage-locations/:id', authMiddleware, async (request, response) => {
|
||||||
try {
|
try {
|
||||||
await store.deleteStorageLocation(request.params.id);
|
await store.deleteStorageLocation(request.params.id);
|
||||||
|
|
|
||||||
14
src/store.js
14
src/store.js
|
|
@ -58,6 +58,7 @@ class JsonStore {
|
||||||
backupScope: profileInput.backupScope || 'volumes',
|
backupScope: profileInput.backupScope || 'volumes',
|
||||||
volumeSelections: profileInput.volumeSelections || {},
|
volumeSelections: profileInput.volumeSelections || {},
|
||||||
backupDir: profileInput.backupDir,
|
backupDir: profileInput.backupDir,
|
||||||
|
storageLocationId: profileInput.storageLocationId || null,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
createdAt: profileInput.createdAt || now,
|
createdAt: profileInput.createdAt || now,
|
||||||
};
|
};
|
||||||
|
|
@ -166,8 +167,21 @@ class JsonStore {
|
||||||
return location;
|
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) {
|
async deleteStorageLocation(locationId) {
|
||||||
await this.write((data) => {
|
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);
|
data.storageLocations = data.storageLocations.filter((item) => item.id !== locationId);
|
||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue