Upload-Serie: Screen-Aus bricht nicht mehr ab, Duplikate erkannt [deploy]
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 16s

Schaltet sich das Display waehrend einer Foto-Serie ab, friert der Browser die
laufenden fetch-Aufrufe ein — die halbe Serie blieb liegen, der zweite Anlauf
erzeugte Duplikate (28.08.2026, Auftrag PROV105: 11 doppelte Fotos).

- Wake Lock waehrend syncQueue(), inkl. Neuanforderung nach App-Wechsel
  (visibilitychange). Scheitert die Anforderung, laeuft der Sync unveraendert
  weiter — die Queue faengt Abbrueche ohnehin ab
- duplicate:true aus der Server-Antwort wandert durch das photo-uploaded-Event;
  die Miniatur zeigt dann „War schon hochgeladen" statt eines zweiten Fotos
- api.js: Kommentar auf den neuen Server-Vertrag gebracht (Bericht 1.6.0 legt
  identische Dateien nicht erneut ab, at-least-once bleibt Rueckfallebene)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eddy 2026-08-28 18:41:56 +02:00
parent 10d5f915df
commit 65bc09d9f1
3 changed files with 38 additions and 3 deletions

2
app.js
View file

@ -3117,6 +3117,8 @@ async function openCameraModal(orderId) {
if (d.relpath) thumb.dataset.relpath = d.relpath; if (d.relpath) thumb.dataset.relpath = d.relpath;
thumb.classList.remove('pending', 'saving'); thumb.classList.remove('pending', 'saving');
thumb.classList.add('done'); 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); window.addEventListener('photo-uploaded', onUploaded);

View file

@ -113,9 +113,11 @@
// Semantik: at-least-once. Geht die Server-Antwort NACH dem Speichern verloren // 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 // (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 // wiederholt. Der Server vergleicht seit Bericht 1.6.0 den Inhalt (md5) gegen die
// enthält uniqid(), es wird also nicht überschrieben). Bewusst gewählt: lieber ein // Dateien im Auftragsordner und legt eine identische Datei kein zweites Mal ab —
// löschbares Duplikat als ein verlorenes Foto. // 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) { async function uploadOrderPhoto(orderId, fileBlob, filename) {
const fd = new FormData(); const fd = new FormData();
fd.append('file', fileBlob, filename || 'photo.jpg'); fd.append('file', fileBlob, filename || 'photo.jpg');

View file

@ -51,12 +51,41 @@
return false; 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() { async function syncQueue() {
// Läuft schon einer? Dann nur merken, dass danach nochmal geprüft werden soll // 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). // (so werden Fotos erfasst, die während eines laufenden Syncs neu dazukamen).
if (syncing) { syncAgain = true; return; } if (syncing) { syncAgain = true; return; }
if (!navigator.onLine) return; if (!navigator.onLine) return;
syncing = true; syncing = true;
await acquireWakeLock();
try { try {
do { do {
syncAgain = false; syncAgain = false;
@ -74,6 +103,7 @@
detail: { detail: {
queueId: it.id, queueId: it.id,
relpath: res && res.relpath, relpath: res && res.relpath,
duplicate: !!(res && res.duplicate),
orderId: it.order_id, orderId: it.order_id,
filename: it.filename, filename: it.filename,
}, },
@ -98,6 +128,7 @@
} while (syncAgain); } while (syncAgain);
} finally { } finally {
syncing = false; syncing = false;
releaseWakeLock();
await updateBadge(); await updateBadge();
emitChange(); emitChange();
} }