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
|
* Backup wiederherstellen
|
||||||
* POST /api/settings/backup/:name/restore
|
* 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) {
|
export async function restoreBackup(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { name } = req.params;
|
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`,
|
message: `${result.restoredRecords} Datensätze und ${result.restoredFiles || 0} Dateien wiederhergestellt`,
|
||||||
});
|
});
|
||||||
} else {
|
} 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) {
|
} 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 { useAuth } from '../../context/AuthContext';
|
||||||
import Button from '../../components/ui/Button';
|
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;
|
const data = err?.response?.data;
|
||||||
if (data) {
|
if (data && typeof data === 'object') {
|
||||||
if (data.error && data.details) return `${data.error}: ${data.details}`;
|
return {
|
||||||
if (data.error) return data.error;
|
headline: data.error || 'Unbekannter Fehler',
|
||||||
if (typeof data === 'string') return data;
|
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() {
|
export default function DatabaseBackup() {
|
||||||
@@ -55,7 +58,9 @@ export default function DatabaseBackup() {
|
|||||||
// Bei Fehler bleibt das Dialog absichtlich offen, damit der User
|
// Bei Fehler bleibt das Dialog absichtlich offen, damit der User
|
||||||
// die Detail-Message sehen + ggf. erneut versuchen kann.
|
// die Detail-Message sehen + ggf. erneut versuchen kann.
|
||||||
onError: (err: any) => {
|
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.
|
<strong>Achtung:</strong> Bestehende Daten und Dokumente werden mit dem Backup-Stand überschrieben.
|
||||||
Dies kann nicht rückgängig gemacht werden.
|
Dies kann nicht rückgängig gemacht werden.
|
||||||
</p>
|
</p>
|
||||||
{restoreMutation.isError && (
|
{restoreMutation.isError && (() => {
|
||||||
<div className="mb-4 bg-red-50 border border-red-200 text-red-800 text-sm p-3 rounded-lg">
|
const e = extractError(restoreMutation.error);
|
||||||
<div className="font-semibold mb-1">Wiederherstellung fehlgeschlagen</div>
|
return (
|
||||||
<div className="whitespace-pre-wrap break-words">
|
<div className="mb-4 bg-red-50 border border-red-200 text-red-800 text-sm p-3 rounded-lg">
|
||||||
{extractError(restoreMutation.error)}
|
<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>
|
);
|
||||||
)}
|
})()}
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
Reference in New Issue
Block a user