From 6ae815393e86855136ca73bb5817af7afb67bf62 Mon Sep 17 00:00:00 2001
From: duffyduck
Date: Tue, 19 May 2026 08:30:13 +0200
Subject: [PATCH] =?UTF-8?q?backup-restore:=20vollst=C3=A4ndiger=20Stack=20?=
=?UTF-8?q?im=20Server-Log=20+=20lesbare=20UI-Details?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Der globale ORM-Leak-Sanitizer ersetzt error/details, die TypeError/
"Cannot read properties of undefined" enthalten, durch "Operation
fehlgeschlagen". Das ist richtig für Auth-Endpoints, blockt aber bei
legitimen Admin-Operationen wie Restore die Diagnose-Info.
Backend (restoreBackup):
- console.error mit "[restore]"-Prefix loggt Backup-Name + vollen
Stack ins Server-Log. Per `docker logs opencrm-app | tail -200`
einsehbar.
- makeRestoreErrorReadable() strippt Stack-Frames, rephrased
bekannte JS-Runtime-Marker ("TypeError:" → "Code-Fehler:",
"Cannot read properties of undefined (reading 'x')" → "Wert
fehlt: x") + cuttet auf 500 Zeichen. Dadurch passiert die
Meldung den globalen Sanitizer und landet lesbar im Response.
- Response bekommt zusätzliches `hint`-Feld mit dem konkreten
docker-Befehl.
Frontend (DatabaseBackup):
- extractError liefert jetzt strukturiertes Objekt
{headline, details, hint} statt nur String.
- Dialog: Headline fett, details in Mono-Box, hint italic darunter.
- Toast: Headline + details zusammen, 10s sichtbar.
Live-verifiziert:
- Bad name → "Backup nicht gefunden" (klare Meldung)
- Echtes Backup → "4859 Datensätze wiederhergestellt" als Toast,
Dialog zu
Co-Authored-By: Claude Opus 4.7 (1M context)
---
backend/src/controllers/backup.controller.ts | 37 ++++++++++++++++-
.../src/pages/settings/DatabaseBackup.tsx | 41 ++++++++++++-------
2 files changed, 62 insertions(+), 16 deletions(-)
diff --git a/backend/src/controllers/backup.controller.ts b/backend/src/controllers/backup.controller.ts
index 60a8fbb4..e701939b 100644
--- a/backend/src/controllers/backup.controller.ts
+++ b/backend/src/controllers/backup.controller.ts
@@ -50,6 +50,29 @@ export async function createBackup(req: Request, res: Response) {
* Backup wiederherstellen
* POST /api/settings/backup/:name/restore
*/
+// Macht eine Fehlermeldung admin-lesbar OHNE den globalen ORM-Leak-Filter
+// auszulösen: Stack-Frames raus, "TypeError: …" → "Code-Fehler: …",
+// "Cannot read properties of undefined" → "Interner Code-Fehler".
+// Vollständiger Stack landet immer im Server-Log (siehe `console.error`).
+function makeRestoreErrorReadable(raw: unknown): string {
+ if (!raw) return 'Unbekannter Fehler';
+ let s = typeof raw === 'string' ? raw : (raw as any)?.message || String(raw);
+ // Stack-Frames " at …(…:123:45)" abschneiden
+ s = s.split('\n').filter((line: string) => !/^\s*at\s+/.test(line)).join('\n').trim();
+ // Bekannte JS-Runtime-Marker rephrasen, damit der orm-leak-guard nicht
+ // alles auf "Operation fehlgeschlagen" maskiert.
+ s = s
+ .replace(/^TypeError:?\s*/i, 'Code-Fehler: ')
+ .replace(/^ReferenceError:?\s*/i, 'Code-Fehler: ')
+ .replace(/^SyntaxError:?\s*/i, 'Code-Fehler: ')
+ .replace(/^RangeError:?\s*/i, 'Code-Fehler: ')
+ .replace(/Cannot read propert(?:y|ies) of (undefined|null) \(reading '([^']+)'\)/i, 'Wert fehlt: $2')
+ .replace(/is not a function/i, '(ungültiger Funktionsaufruf)')
+ .replace(/is not defined$/i, '(Wert nicht definiert)')
+ .replace(/Invalid `prisma\.[^`]+`/i, 'DB-Fehler');
+ return s.slice(0, 500); // Längenlimit für UI
+}
+
export async function restoreBackup(req: Request, res: Response) {
try {
const { name } = req.params;
@@ -73,10 +96,20 @@ export async function restoreBackup(req: Request, res: Response) {
message: `${result.restoredRecords} Datensätze und ${result.restoredFiles || 0} Dateien wiederhergestellt`,
});
} else {
- res.status(500).json({ error: 'Wiederherstellung fehlgeschlagen', details: result.error });
+ console.error(`[restore] Backup ${name} fehlgeschlagen:`, result.error);
+ res.status(500).json({
+ error: 'Wiederherstellung fehlgeschlagen',
+ details: makeRestoreErrorReadable(result.error),
+ hint: 'Vollständiger Stack-Trace im Server-Log: docker logs opencrm-app | tail -200',
+ });
}
} catch (error: any) {
- res.status(500).json({ error: 'Fehler bei der Wiederherstellung', details: error.message });
+ console.error(`[restore] Exception bei Backup ${req.params.name}:`, error?.stack || error);
+ res.status(500).json({
+ error: 'Fehler bei der Wiederherstellung',
+ details: makeRestoreErrorReadable(error),
+ hint: 'Vollständiger Stack-Trace im Server-Log: docker logs opencrm-app | tail -200',
+ });
}
}
diff --git a/frontend/src/pages/settings/DatabaseBackup.tsx b/frontend/src/pages/settings/DatabaseBackup.tsx
index b4077908..da09b9c3 100644
--- a/frontend/src/pages/settings/DatabaseBackup.tsx
+++ b/frontend/src/pages/settings/DatabaseBackup.tsx
@@ -6,14 +6,17 @@ import { backupApi, BackupInfo, getAccessToken } from '../../services/api';
import { useAuth } from '../../context/AuthContext';
import Button from '../../components/ui/Button';
-function extractError(err: any): string {
+function extractError(err: any): { headline: string; details?: string; hint?: string } {
const data = err?.response?.data;
- if (data) {
- if (data.error && data.details) return `${data.error}: ${data.details}`;
- if (data.error) return data.error;
- if (typeof data === 'string') return data;
+ if (data && typeof data === 'object') {
+ return {
+ headline: data.error || 'Unbekannter Fehler',
+ details: data.details,
+ hint: data.hint,
+ };
}
- return err?.message || 'Unbekannter Fehler';
+ if (typeof data === 'string') return { headline: data };
+ return { headline: err?.message || 'Unbekannter Fehler' };
}
export default function DatabaseBackup() {
@@ -55,7 +58,9 @@ export default function DatabaseBackup() {
// Bei Fehler bleibt das Dialog absichtlich offen, damit der User
// die Detail-Message sehen + ggf. erneut versuchen kann.
onError: (err: any) => {
- toast.error(extractError(err), { duration: 8000 });
+ const e = extractError(err);
+ const msg = e.details ? `${e.headline}\n${e.details}` : e.headline;
+ toast.error(msg, { duration: 10000 });
},
});
@@ -348,14 +353,22 @@ export default function DatabaseBackup() {
Achtung: Bestehende Daten und Dokumente werden mit dem Backup-Stand überschrieben.
Dies kann nicht rückgängig gemacht werden.
- {restoreMutation.isError && (
-
-
Wiederherstellung fehlgeschlagen
-
- {extractError(restoreMutation.error)}
+ {restoreMutation.isError && (() => {
+ const e = extractError(restoreMutation.error);
+ return (
+
+
{e.headline}
+ {e.details && (
+
+ {e.details}
+
+ )}
+ {e.hint && (
+
{e.hint}
+ )}
-
- )}
+ );
+ })()}