Fotos gingen beim Screen-Aus verloren: Verkleinern konnte ewig haengen [deploy]
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 14s

Eddy: „beim ersten Upload nach Screen schliessen kamen nicht mehr alle an, ich
habe noch die gruenen Labels gesehen, aber die Bilder waren nicht da".

Ursache: uploadPhoto() verkleinert das Foto, BEVOR es in die Queue geschrieben
wird. resizeImage() haengt an img.onload / canvas.toBlob und hatte keinen
Timeout — schaltet sich das Display ab, pausiert der Browser Decoding und
Canvas, es feuert weder onload noch onerror, das Promise loest nie auf. Damit
war das Foto NIRGENDS gesichert (nicht in der Queue, nicht am Server), und weil
die Galerie-Auswahl sequentiell laeuft, stand die komplette restliche Auswahl.
Deshalb hat auch nichts automatisch nachgeladen: es gab nichts nachzuholen.
An Auftrag (PROV105) fehlten so 8 von 19 Fotos.

- resizeImage() bekommt eine Schutzzeit (Promise.race, 15s): im Zweifel wird
  das unverkleinerte Original gesichert statt gar nichts
- Auswahl-Schleife: jedes Foto einzeln abgesichert, ein Fehlschlag blockiert die
  restliche Auswahl nicht mehr
- Wake Lock schon waehrend der Auswahl-Verarbeitung, nicht erst beim Sync — das
  Verkleinern ist genau die Stelle, die pausiert wurde. Refcount, weil sich
  Auswahl-Schleife und Sync verschachteln

Schutzzeit isoliert geprueft (haengendes Decoding simuliert): faellt nach der
Frist auf das Original zurueck statt zu blockieren.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-08-28 18:58:35 +02:00
parent 65bc09d9f1
commit 130d9d1ec3
2 changed files with 38 additions and 6 deletions

29
app.js
View file

@ -907,8 +907,20 @@ router.on('/orders/:id', async (args) => {
document.getElementById('btn-pick-photo').onclick = () => galInput.click(); document.getElementById('btn-pick-photo').onclick = () => galInput.click();
async function handleFiles(files) { async function handleFiles(files) {
for (const f of files) { // Screen wachhalten, solange die Auswahl abgearbeitet wird — sonst pausiert
await uploadPhoto(args.id, f); // das Verkleinern mitten in der Serie (siehe resizeImage).
await offline.acquireWakeLock();
try {
for (const f of files) {
// Ein einzelnes Foto darf die restliche Auswahl nicht blockieren
try {
await uploadPhoto(args.id, f);
} catch (e) {
showToast('Foto ' + (f.name || '') + ' fehlgeschlagen: ' + ((e && e.message) || ''), 'error');
}
}
} finally {
offline.releaseWakeLock();
} }
// Nach Upload einfach die Route neu rendern — so werden Select-Mode, // Nach Upload einfach die Route neu rendern — so werden Select-Mode,
// Click-Handler und Thumbnails sauber neu aufgebaut. // Click-Handler und Thumbnails sauber neu aufgebaut.
@ -935,8 +947,15 @@ async function uploadPhoto(orderId, file) {
await offline.syncQueue(); await offline.syncQueue();
} }
async function resizeImage(file, maxSide) { async function resizeImage(file, maxSide, timeoutMs) {
return new Promise((resolve) => { // SCHUTZZEIT (v1.1): Schaltet sich das Display waehrend einer Serie ab, pausiert der
// Browser Bild-Decoding und Canvas — dann feuert weder onload noch onerror und das
// Promise loeste NIE auf. Das passiert VOR dem Sichern in der Queue, die betroffenen
// Fotos waren also nirgends gespeichert, und weil die Auswahl sequentiell laeuft,
// stand die komplette restliche Auswahl still (28.08.2026, Auftrag PROV105: 8 von 19
// Fotos kamen nie an und waren auch nicht in der Queue zum Nachholen).
// Im Zweifel lieber das unverkleinerte Original sichern als gar nichts.
const shrink = new Promise((resolve) => {
const img = new Image(); const img = new Image();
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
img.onload = () => { img.onload = () => {
@ -952,6 +971,8 @@ async function resizeImage(file, maxSide) {
img.onerror = () => { URL.revokeObjectURL(url); resolve(file); }; img.onerror = () => { URL.revokeObjectURL(url); resolve(file); };
img.src = url; img.src = url;
}); });
const guard = new Promise((resolve) => setTimeout(() => resolve(file), timeoutMs || 15000));
return Promise.race([shrink, guard]);
} }
/** /**

View file

@ -56,9 +56,14 @@
// Serie bleibt liegen (real am 28.08.2026: halbe Serie kam nicht an, der zweite Anlauf // Serie bleibt liegen (real am 28.08.2026: halbe Serie kam nicht an, der zweite Anlauf
// erzeugte dann Duplikate). Scheitert die Anforderung (Akkusparmodus, alter Browser), // erzeugte dann Duplikate). Scheitert die Anforderung (Akkusparmodus, alter Browser),
// laeuft der Sync unveraendert weiter — die Queue faengt den Abbruch ohnehin ab. // laeuft der Sync unveraendert weiter — die Queue faengt den Abbruch ohnehin ab.
// Referenzzaehler, weil zwei Stellen den Lock brauchen und sich verschachteln:
// die Auswahl-Schleife (Verkleinern + Sichern) und der Sync selbst. Wer zuerst
// fertig ist, darf dem anderen den Lock nicht wegnehmen.
let wakeLock = null; let wakeLock = null;
let wakeRefs = 0;
async function acquireWakeLock() { async function acquireWakeLock() {
wakeRefs++;
try { try {
if (!navigator.wakeLock || wakeLock) return; if (!navigator.wakeLock || wakeLock) return;
wakeLock = await navigator.wakeLock.request('screen'); wakeLock = await navigator.wakeLock.request('screen');
@ -67,6 +72,8 @@
} }
function releaseWakeLock() { function releaseWakeLock() {
wakeRefs = Math.max(0, wakeRefs - 1);
if (wakeRefs > 0) return;
try { if (wakeLock) wakeLock.release(); } catch (_) {} try { if (wakeLock) wakeLock.release(); } catch (_) {}
wakeLock = null; wakeLock = null;
} }
@ -75,7 +82,10 @@
// App-Wechsel). Kommt sie zurueck und laeuft noch ein Sync, neu anfordern. // App-Wechsel). Kommt sie zurueck und laeuft noch ein Sync, neu anfordern.
try { try {
document.addEventListener('visibilitychange', function () { document.addEventListener('visibilitychange', function () {
if (document.visibilityState === 'visible' && syncing) acquireWakeLock(); if (document.visibilityState === 'visible' && wakeRefs > 0 && !wakeLock) {
wakeRefs--; // acquireWakeLock zaehlt gleich wieder hoch
acquireWakeLock();
}
}); });
} catch (_) {} } catch (_) {}
@ -203,5 +213,6 @@
setInterval(() => { if (navigator.onLine) syncQueue(); }, 15000); setInterval(() => { if (navigator.onLine) syncQueue(); }, 15000);
}); });
window.offline = { enqueuePhoto, syncQueue, updateBadge, queueCount, listQueue, retryFailed }; window.offline = { enqueuePhoto, syncQueue, updateBadge, queueCount, listQueue, retryFailed,
acquireWakeLock, releaseWakeLock };
})(); })();