Fotos gingen beim Screen-Aus verloren: Verkleinern konnte ewig haengen [deploy]
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 14s
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:
parent
65bc09d9f1
commit
130d9d1ec3
2 changed files with 38 additions and 6 deletions
25
app.js
25
app.js
|
|
@ -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) {
|
||||||
|
// Screen wachhalten, solange die Auswahl abgearbeitet wird — sonst pausiert
|
||||||
|
// das Verkleinern mitten in der Serie (siehe resizeImage).
|
||||||
|
await offline.acquireWakeLock();
|
||||||
|
try {
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
|
// Ein einzelnes Foto darf die restliche Auswahl nicht blockieren
|
||||||
|
try {
|
||||||
await uploadPhoto(args.id, f);
|
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]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue