fix: Image-Load robust + Schutz gegen Speichern ohne Bild
All checks were successful
Deploy bericht / deploy (push) Successful in 1s

Diagnose: bei alten Berichten ohne composite_path wurde das bgImage
beim Öffnen nicht geladen (fabric.Image.fromURL mit crossOrigin bei
Blob-URL hing sich auf). Danach hat jeder Save das Composite ohne
Bild gespeichert → PDF-Seite leer.

Fix 1 — renderImage:
- Native Image() statt fabric.Image.fromURL
- Kein crossOrigin bei Blob-URLs (überflüssig, verursachte Hänger)
- Timeout 10s mit console.warn
- onerror mit Debug-Info (mime, bufSize)
- applyTool() nach Einfügen damit Tool-Lock greift

Fix 2 — savePageAnnotations:
- Wenn KEIN Bild im Canvas ist (weder bgImage=true noch type=image),
  wird der Save ABGEBROCHEN statt ein leeres Composite zu speichern.
- Dadurch kann ein alter Bericht nicht versehentlich mit einem
  leeren Composite überschrieben werden wenn der Bild-Load hakt.
- Toast-Warnung für den User, console.warn für Debug.

Fix 3 — finalize-Handler:
- Native confirm() vor dem Finalisieren
- Bessere Fehler-Diagnose: lese Response als text(), parse JSON,
  logge bei JSON-Parse-Fehler den Raw-Body
- toast() statt alert() für Fehler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
This commit is contained in:
Eduard Wisch 2026-04-09 14:58:18 +02:00
parent 01fbd72310
commit 9c7ef73061

View file

@ -270,35 +270,52 @@
const blob = new Blob([arrayBuffer], { type: mime }); const blob = new Blob([arrayBuffer], { type: mime });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
// Native Image-Load + fabric.Image(element) — kein CORS auf Blob-URLs,
// bessere Fehler-Diagnose, Timeout-Fallback
return new Promise((res) => { return new Promise((res) => {
fabric.Image.fromURL(url, (fabricImg) => { const htmlImg = new Image();
// Wenn schon ein Bild-Objekt im Canvas ist (nach re-render), const tid = setTimeout(() => {
// altes entfernen bevor neues rein kommt console.warn('[renderImage] Timeout beim Bild-Laden nach 10s — URL:', url);
const existing = fabricCanvas.getObjects().find(o => o.bgImage === true);
if (existing) fabricCanvas.remove(existing);
// Markiere dieses Objekt als "Hintergrundbild" (bleibt aber editierbar)
fabricImg.bgImage = true;
// Auf Canvas-Größe skalieren (contain)
const imgRatio = Math.min(canvasW / fabricImg.width, canvasH / fabricImg.height);
fabricImg.scale(imgRatio);
fabricImg.set({
left: (canvasW - fabricImg.width * imgRatio) / 2,
top: (canvasH - fabricImg.height * imgRatio) / 2,
angle: currentPageRotation,
selectable: true,
hasControls: true,
hasBorders: true,
lockRotation: false,
});
// Ziehbares Hintergrundbild ganz nach hinten
fabricCanvas.add(fabricImg);
fabricImg.sendToBack();
fabricCanvas.requestRenderAll();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
res(); res();
}, { crossOrigin: 'anonymous' }); }, 10000);
htmlImg.onload = () => {
clearTimeout(tid);
try {
const fabricImg = new fabric.Image(htmlImg);
const existing = fabricCanvas.getObjects().find(o => o.bgImage === true);
if (existing) fabricCanvas.remove(existing);
fabricImg.bgImage = true;
const imgRatio = Math.min(canvasW / fabricImg.width, canvasH / fabricImg.height);
fabricImg.scale(imgRatio);
fabricImg.set({
left: (canvasW - fabricImg.width * imgRatio) / 2,
top: (canvasH - fabricImg.height * imgRatio) / 2,
angle: currentPageRotation,
selectable: true,
hasControls: true,
hasBorders: true,
lockRotation: false,
});
fabricCanvas.add(fabricImg);
fabricImg.sendToBack();
fabricCanvas.requestRenderAll();
if (typeof applyTool === 'function') applyTool();
} catch (e) {
console.error('[renderImage] Fabric-Wrap fehlgeschlagen:', e);
}
URL.revokeObjectURL(url);
res();
};
htmlImg.onerror = (err) => {
clearTimeout(tid);
console.error('[renderImage] htmlImg onerror:', err, 'mime:', mime, 'bufSize:', arrayBuffer.byteLength);
URL.revokeObjectURL(url);
res();
};
htmlImg.src = url;
}); });
} }
@ -713,15 +730,25 @@
/* ---------- Speichern ---------- */ /* ---------- Speichern ---------- */
async function savePageAnnotations(showMessage = true) { async function savePageAnnotations(showMessage = true) {
if (!currentPageId) return; if (!currentPageId) return;
// Sicherheit: wenn kein bgImage im Canvas ist, NICHT speichern — sonst
// überschreiben wir einen funktionierenden Bericht mit einem leeren
// Composite
const hasBg = fabricCanvas.getObjects().some(o => o.bgImage === true || o.type === 'image');
if (!hasBg) {
console.warn('[savePageAnnotations] Kein Bild im Canvas — skip, um Datenverlust zu vermeiden');
if (showMessage) toast('Speichern übersprungen (kein Bild geladen)', 'warn');
return;
}
const fd = new FormData(); const fd = new FormData();
fd.append('token', cfg.token); fd.append('token', cfg.token);
fd.append('pageid', currentPageId); fd.append('pageid', currentPageId);
// Fabric-JSON MIT Hintergrundbild (bgImage-Objekte werden serialisiert)
fd.append('fabric_json', JSON.stringify(fabricCanvas.toJSON(['bgImage']))); fd.append('fabric_json', JSON.stringify(fabricCanvas.toJSON(['bgImage'])));
fd.append('note', document.getElementById('page-note').value || ''); fd.append('note', document.getElementById('page-note').value || '');
fd.append('rotation', currentPageRotation); fd.append('rotation', currentPageRotation);
// Phase 6: Composite-PNG der gesamten Seite generieren und mitschicken // Phase 6: Composite-PNG
try { try {
// Selektion aufheben damit keine Controls gerendert werden // Selektion aufheben damit keine Controls gerendert werden
const active = fabricCanvas.getActiveObject(); const active = fabricCanvas.getActiveObject();
@ -821,17 +848,32 @@
} }
document.getElementById('btn-finalize').addEventListener('click', async () => { document.getElementById('btn-finalize').addEventListener('click', async () => {
if (!confirm('Bericht jetzt finalisieren und PDF erzeugen?')) return;
toast('Speichere aktuelle Seite…');
await savePageAnnotations(false); await savePageAnnotations(false);
const fd = new FormData(); toast('PDF wird erzeugt…');
fd.append('token', cfg.token); try {
fd.append('berichtid', cfg.berichtid); const fd = new FormData();
const r = await fetch(cfg.urls.generate_pdf, { method: 'POST', body: fd }); fd.append('token', cfg.token);
const data = await r.json(); fd.append('berichtid', cfg.berichtid);
if (data.success) { const r = await fetch(cfg.urls.generate_pdf, { method: 'POST', body: fd });
toast('PDF erstellt: ' + data.filename); const txt = await r.text();
setTimeout(() => location.reload(), 1500); let data = {};
} else { try { data = JSON.parse(txt); } catch (e) {
alert('Fehler: ' + (data.error || 'unbekannt')); console.error('generate_pdf lieferte kein JSON:', txt);
toast('Server-Fehler (kein JSON)', 'error');
return;
}
if (data.success) {
toast('✓ PDF erstellt: ' + data.filename);
setTimeout(() => location.reload(), 1500);
} else {
console.error('generate_pdf failed:', data);
toast('Fehler: ' + (data.error || 'unbekannt'), 'error');
}
} catch (e) {
console.error('finalize exception:', e);
toast('Netzwerk-Fehler: ' + e.message, 'error');
} }
}); });
} }