diff --git a/app.js b/app.js index 5edd05e..808949e 100644 --- a/app.js +++ b/app.js @@ -3117,6 +3117,8 @@ async function openCameraModal(orderId) { if (d.relpath) thumb.dataset.relpath = d.relpath; thumb.classList.remove('pending', 'saving'); thumb.classList.add('done'); + // Server hatte das Bild schon (Wiederholung nach Abbruch): kein Fehler, nur Hinweis + if (d.duplicate) thumb.title = 'War schon hochgeladen'; } window.addEventListener('photo-uploaded', onUploaded); diff --git a/lib/api.js b/lib/api.js index bf85a5a..e1e0047 100644 --- a/lib/api.js +++ b/lib/api.js @@ -113,9 +113,11 @@ // Semantik: at-least-once. Geht die Server-Antwort NACH dem Speichern verloren // (z.B. Netz-Abbruch, Proxy-2xx-HTML), gilt der Upload als unbestätigt und wird - // wiederholt → das Foto kann dadurch doppelt auf dem Server landen (der Dateiname - // enthält uniqid(), es wird also nicht überschrieben). Bewusst gewählt: lieber ein - // löschbares Duplikat als ein verlorenes Foto. + // wiederholt. Der Server vergleicht seit Bericht 1.6.0 den Inhalt (md5) gegen die + // Dateien im Auftragsordner und legt eine identische Datei kein zweites Mal ab — + // er antwortet dann mit duplicate:true und dem relpath der vorhandenen Datei. + // Die Wiederholung ist damit idempotent statt duplikaterzeugend; die Regel „lieber + // ein Duplikat als ein verlorenes Foto" gilt weiterhin als Rückfallebene. async function uploadOrderPhoto(orderId, fileBlob, filename) { const fd = new FormData(); fd.append('file', fileBlob, filename || 'photo.jpg'); diff --git a/lib/offline.js b/lib/offline.js index c721f27..e9512cc 100644 --- a/lib/offline.js +++ b/lib/offline.js @@ -51,12 +51,41 @@ return false; } + // Screen wachhalten, solange Uploads laufen. Schaltet sich das Display waehrend einer + // Foto-Serie ab, friert der Browser die laufenden fetch-Aufrufe ein und der Rest der + // 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), + // laeuft der Sync unveraendert weiter — die Queue faengt den Abbruch ohnehin ab. + let wakeLock = null; + + async function acquireWakeLock() { + try { + if (!navigator.wakeLock || wakeLock) return; + wakeLock = await navigator.wakeLock.request('screen'); + wakeLock.addEventListener('release', function () { wakeLock = null; }); + } catch (_) { wakeLock = null; } + } + + function releaseWakeLock() { + try { if (wakeLock) wakeLock.release(); } catch (_) {} + wakeLock = null; + } + + // Ein Wake Lock geht verloren, sobald die Seite in den Hintergrund geht (Anruf, + // App-Wechsel). Kommt sie zurueck und laeuft noch ein Sync, neu anfordern. + try { + document.addEventListener('visibilitychange', function () { + if (document.visibilityState === 'visible' && syncing) acquireWakeLock(); + }); + } catch (_) {} + async function syncQueue() { // Läuft schon einer? Dann nur merken, dass danach nochmal geprüft werden soll // (so werden Fotos erfasst, die während eines laufenden Syncs neu dazukamen). if (syncing) { syncAgain = true; return; } if (!navigator.onLine) return; syncing = true; + await acquireWakeLock(); try { do { syncAgain = false; @@ -74,6 +103,7 @@ detail: { queueId: it.id, relpath: res && res.relpath, + duplicate: !!(res && res.duplicate), orderId: it.order_id, filename: it.filename, }, @@ -98,6 +128,7 @@ } while (syncAgain); } finally { syncing = false; + releaseWakeLock(); await updateBadge(); emitChange(); }