Backup-Operations-Log + EBUSY-Fix beim Restore

Backup-Seite zeigt zwei neue Log-Panels: links Backup-Erstellung,
rechts Backup-Wiederherstellung. Jeder Eintrag mit ✓/✗-Status,
Summary, Timestamp + User. Klick öffnet Modal mit vollständigem
Verlauf – alle console.log/error/warn/info-Zeilen werden während
der Operation in einen Puffer mitgefangen und im fullLog-Feld
persistiert. Auto-Refresh alle 5s.

Persistenz: neue Tabelle BackupLog mit Migration
20260519100000_backup_log (CREATE TABLE IF NOT EXISTS für Re-Deploys
auf DBs mit Vorab-db-push). fullLog auf 1 MB gecappt.

Endpoints (settings:update):
- GET /api/settings/backup-logs?operation=CREATE|RESTORE&limit=50
- GET /api/settings/backup-logs/:id

EBUSY-Fix: Der neue Log-Verlauf hat sofort einen alten Bug
sichtbar gemacht. backup.service.restoreBackup rief
deleteDirectory(UPLOADS_DIR) auf, dessen finales rmdirSync auf
/app/uploads ein EBUSY warf – das Verzeichnis ist im Container ein
Bind-Mount und lässt sich nicht aushängen. Fix: neuer Helper
emptyDirectory() löscht nur die Inhalte, das Verzeichnis bleibt
stehen.

Live-verifiziert: 4867 Datensätze + 1 Datei in 13.2s
wiederhergestellt; Log-Modal zeigt den vollständigen Verlauf.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 11:53:04 +02:00
parent 95541e8ac4
commit 37df8c0c4a
8 changed files with 506 additions and 15 deletions
+27
View File
@@ -1012,6 +1012,33 @@ export const backupApi = {
},
};
export interface BackupLogEntry {
id: number;
operation: 'CREATE' | 'RESTORE';
backupName: string | null;
success: boolean;
durationMs: number;
summary: string;
userEmail: string | null;
ipAddress: string | null;
createdAt: string;
}
export interface BackupLogDetail extends BackupLogEntry {
fullLog: string;
}
export const backupLogApi = {
list: async (operation: 'CREATE' | 'RESTORE') => {
const res = await api.get<ApiResponse<BackupLogEntry[]>>('/settings/backup-logs', {
params: { operation, limit: 50 },
});
return res.data;
},
get: async (id: number) => {
const res = await api.get<ApiResponse<BackupLogDetail>>(`/settings/backup-logs/${id}`);
return res.data;
},
};
// Rate-Limit-Verwaltung (Admin)
export interface ActiveRateLimit {
ipAddress: string;