backup-restore: vollständiger Stack im Server-Log + lesbare UI-Details
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
<strong>Achtung:</strong> Bestehende Daten und Dokumente werden mit dem Backup-Stand überschrieben.
|
||||
Dies kann nicht rückgängig gemacht werden.
|
||||
</p>
|
||||
{restoreMutation.isError && (
|
||||
<div className="mb-4 bg-red-50 border border-red-200 text-red-800 text-sm p-3 rounded-lg">
|
||||
<div className="font-semibold mb-1">Wiederherstellung fehlgeschlagen</div>
|
||||
<div className="whitespace-pre-wrap break-words">
|
||||
{extractError(restoreMutation.error)}
|
||||
{restoreMutation.isError && (() => {
|
||||
const e = extractError(restoreMutation.error);
|
||||
return (
|
||||
<div className="mb-4 bg-red-50 border border-red-200 text-red-800 text-sm p-3 rounded-lg">
|
||||
<div className="font-semibold mb-1">{e.headline}</div>
|
||||
{e.details && (
|
||||
<div className="whitespace-pre-wrap break-words font-mono text-xs bg-white border border-red-100 rounded p-2 mb-2">
|
||||
{e.details}
|
||||
</div>
|
||||
)}
|
||||
{e.hint && (
|
||||
<div className="text-xs text-red-600 italic">{e.hint}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
})()}
|
||||
<div className="flex justify-end gap-3">
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
Reference in New Issue
Block a user