From 64ebbf7850ded8853485d1f72fb70981c6ecd007 Mon Sep 17 00:00:00 2001 From: Eddy Date: Fri, 18 Sep 2026 23:36:14 +0200 Subject: [PATCH] Rauswurf aus dem Auftrag behoben, wartende Fotos am Auftrag sichtbar Gemeldet 18.09.2026: nach "Fertig" in der Kamera stand die App auf der Auftragsliste, und im Auftrag war von den Fotos nichts zu sehen. Ursache Rauswurf (Prod-Log 17.09., Auftrag 111): closeModal() ruft history.back() (asynchron), direkt danach setzt router.go() den Hash. Der Ruecksprung laeuft erst nach dem Hash-Wechsel und springt hinter den neuen Eintrag zurueck - angezeigt wird der Auftrag, in der Adresse steht '#/orders'. Das naechste router.navigate() (Kamera "Fertig") zeichnet dann die Liste. Betraf nur in derselben Sitzung ueber den Plus-Knopf angelegte Auftraege. - closeModal(): Zaehler _pendingBacks statt Boolean (KB #1209), History- Eintrag wird vor dem Cleanup zurueckgenommen - router.go() und pushModal() warten ueber historySettled() auf ausstehende Ruecksprunge (Schutzzeit 1,5 s) - Kamera und Galerie-Auswahl steuern nach dem Sichern ausdruecklich '#/orders/' an statt den Hash der Adresszeile - Auftragsseite: Block "Warten auf Upload (n)" mit Vorschaubildern aus der Warteschlange und Klartext; zieht nach photo-uploaded selbst nach - Hinweis-Toast beim Schliessen der Kamera, wenn noch Fotos warten Verifiziert lokal in Chromium mit echtem Offline-Modus; die Kamera selbst (getUserMedia) am Handy ist noch nicht getestet. Kein Deploy. Co-Authored-By: Claude Fable 5.1 --- README.md | 1 + ROADMAP.md | 86 ++++++++++++++++++++++- app.css | 31 ++++++++ app.js | 190 ++++++++++++++++++++++++++++++++++++++++++++++---- lib/router.js | 17 +++-- 5 files changed, 304 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 12acf14..7c28ef7 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Mobile Progressive Web App für die Baustellen-Doku — Foto-Upload, Sprach- und - ✅ **Persist-First / Datenverlust-Schutz**: jedes Foto wird beim Auslösen ZUERST in IndexedDB gesichert, Upload erst danach; Queue-Item wird nur nach bestätigtem Upload (HTTP 2xx mit gültigem `relpath`) gelöscht — überlebt fehlendes/schwaches Netz, hängende Uploads (45s-Timeout) und App-Kill. Erkennt 2xx-HTML (abgelaufene Session/Proxy-Loginseite) als Fehler statt als Erfolg - ✅ Auto-Sync bei "online", periodisch (15s) und bei App-Fokus; Status-Badge (🟢 alles gesichert / 🟡 lädt hoch / 🔴 offline / ⚠️ fehlgeschlagen) - ✅ Recovery: Tipp auf das Status-Badge öffnet die Warteschlange → erneut senden / teilen; Warnung beim Schließen mit noch ungesicherten Fotos +- ✅ **Wartende Fotos stehen am Auftrag**: Block „⏳ Warten auf Upload (n)" mit Vorschaubildern aus der Warteschlange und Klartext (gesichert auf dem Gerät, geht raus sobald Netz da ist); nach dem Upload zieht die Auftragsseite von selbst nach. Hinweis-Toast beim Schließen der Kamera, wenn noch etwas wartet - ✅ Kacheln laden **serverseitige Thumbnails** (`photo.php?size=thumb`) statt der Originale — rund 10 KB statt mehrerer hundert KB je Kachel, parallel und mit `loading="lazy"`, Wiederholaufrufe enden mit 304 - ✅ Foto-Viewer mit Zoom + Swipe - ✅ Foto-Skizze: Annotationen mit Pfeilen, Kreisen, Rechtecken, Text diff --git a/ROADMAP.md b/ROADMAP.md index a996086..95dda82 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # ROADMAP — Baustelle PWA -Stand: 2026-08-31 · Gegenstück: `data/bericht` (Modul-Version 1.5.1) +Stand: 2026-09-18 · Gegenstück: `data/bericht` (Modul-Version 1.5.1) Die PWA hat kein eigenes Backend — alles läuft über `custom/bericht/api/`. Punkte, die eine API-Änderung brauchen, sind mit **[API]** markiert und stehen zusätzlich in der ROADMAP des @@ -224,6 +224,90 @@ Keine Schreib-Warteschlange für Auftragsdaten. Die bräuchte Idempotenz und Konfliktbehandlung (zwei Geräte am selben Auftrag). Für Fotos existiert sie längst (Persist-First-Queue), für alles andere gilt: lesen ja, ändern nur mit Netz. +## 6. Rauswurf aus dem Auftrag + unsichtbare Warteschlange (gemeldet 2026-09-18) + +Gemeldet von Eddy: am 17.09.2026 gegen 18 Uhr mehrere Fotos in einem Auftrag gemacht, +„Fertig" getippt — die App stand danach auf der **Auftragsliste** statt im Auftrag. Wieder +in den Auftrag gegangen: **keine Fotos zu sehen**, kein Hinweis, dass sie noch warten. + +### Befund (Prod-Zugriffslog 17.09., Auftrag 111 — kein Raten) + +- 17:30:56 `POST orders.php?action=create` — Auftrag 111 über den ➕-Knopf angelegt. In + derselben Sekunde laufen **zwei** Routen gleichzeitig: `orders.php` (Liste) **und** + `orders.php?id=111` (Detail). Das ist die Signatur des Fehlers. +- 17:40:45 `verify.php` + `orders.php` (Liste) — der Rauswurf. **Kein** `index.php` davor, + also kein Reload, kein SW-Update, kein Logout (kein 401), kein Absturz. +- 17:40:47 Auftrag 111 erneut geöffnet, `action=photos` liefert eine **leere** Liste; + die Uploads kommen erst 17:40:54 und 17:57:25–17:58:22 an. + +**Ursache Rauswurf — History-Race in `openNewOrderModal`:** + +```js +closeModal(modal); // -> history.back() (asynchron!) +router.go('#/orders/' + res.order.id); // -> location.hash = … (sofort) +``` + +`history.back()` wird erst nach dem Hash-Wechsel ausgeführt und springt dann **hinter** den +neuen Eintrag zurück. Ergebnis: Auf dem Bildschirm steht der Auftrag (seine Route wurde als +letzte fertig), in der Adresszeile steht aber `#/orders`. Alles, was danach +`router.navigate()` ruft — z. B. „Fertig" in der Kamera — zeichnet die **Auftragsliste**. +Nachgestellt in Chromium (18.09.2026): ohne Abwarten endet der Ablauf auf `#/orders`, mit +abgewartetem `popstate` auf `#/orders/111`. + +Dieselbe Falle an zwei weiteren Stellen: „Neuen Bericht anlegen" (`#/reports/`) und +Lieferschein-Unterschrift (`#/shipments/`). Betroffen ist nur, wer den Auftrag **in +derselben Sitzung frisch angelegt** hat — deshalb fiel es nicht früher auf. + +**Ursache „keine Fotos, keine Meldung":** Die Auftragsseite zeigt ausschließlich, was der +Server schon hat. Fotos in der Warteschlange erscheinen nirgends außer als Zahl im kleinen +Badge oben rechts (🔴 3 / 🟡 3), und die Seite zeichnet sich nach einem im Hintergrund +fertig gewordenen Upload nicht neu. Verloren ging nichts — alle Fotos kamen an. + +### Umsetzung (2026-09-18) + +- [x] **Race zentral behoben:** `closeModal()` zählt selbst ausgelöste `history.back()` + (`_pendingBacks`, Zähler statt Boolean, KB #1209 — der Boolean verschluckt bei zwei + schnell nacheinander geschlossenen Dialogen ein `popstate`), `router.go()` wartet über + `window.historySettled()`, bis alle durch sind, und wechselt erst dann den Hash + (Schutzzeit 1,5 s, falls ein `popstate` ausbleibt). Alle Aufrufstellen bleiben wie sie + sind. `closeModal()` nimmt den History-Eintrag jetzt **vor** dem Cleanup zurück, damit + ein navigierender Cleanup den Rücksprung sieht; `pushModal()` wartet ebenfalls. +- [x] **Sicherheitsnetz in Kamera und Galerie-Auswahl:** nach „Fertig" ausdrücklich + `router.go('#/orders/')` statt `router.navigate()` — gezeichnet wird der Auftrag, + für den fotografiert wurde, nicht „was gerade in der Adresszeile steht". +- [x] **Wartende Fotos am Auftrag:** Block „⏳ Warten auf Upload (n)" über „Hochgeladene + Fotos", mit Vorschaubildern direkt aus der Warteschlange (max. 12, danach „+n") und + Klartext („📴 Kein Netz — 3 Fotos sind auf dem Gerät gesichert und werden gesendet, + sobald wieder Netz da ist."), fehlgeschlagene rot markiert; Tipp öffnet die Warteschlange. +- [x] **Auftragsseite zieht nach:** bei `photo-uploaded` für diesen Auftrag wird nach 2,5 s + Ruhe neu gezeichnet (Scrollposition bleibt) — nicht, solange ein Modal offen ist, + die Mehrfachauswahl läuft oder eine Sprachnotiz abgespielt wird. +- [x] **Hinweis beim Schließen der Kamera:** „📴 Kein Netz — n Fotos auf dem Gerät + gesichert, Upload folgt automatisch" bzw. „⏳ n Fotos warten noch auf den Upload". + +### Verifiziert (2026-09-18, lokale Testinstanz, Chromium, Auftrag 62) + +- Ablauf „Modal zu + `router.go`": endet auf `#/orders/62` (vorher `#/orders`) +- **Echtes Funkloch** (`context.setOffline(true)`): 3 Fotos gesichert, „Fertig" → bleibt im + Auftrag, Offline-Band, Block mit 3 Vorschaubildern und Klartext, Badge 🔴 3 +- Netz zurück: Upload läuft an, Block verschwindet von selbst, „Hochgeladene Fotos" 2 → 5, + Badge 🟢, Warteschlange leer. Testfotos danach wieder entfernt. +- **Nicht** getestet: die echte Kamera (`getUserMedia`) am Handy — der Test hat den + Kamera-Cleanup nachgestellt, nicht die Kamera selbst bedient. + +### Offen + +- [ ] **Doppel-Uploads** (Log: mehrere Antworten mit `duplicate:true`) — Seite und Service + Worker (Background Sync) laden dasselbe Queue-Item parallel hoch. Dank md5-Abgleich + seit Bericht 1.6.0 folgenlos, kostet aber im Mobilfunk doppeltes Datenvolumen. + Lösung: Sperre je Item in IndexedDB (`uploading_since`), die der jeweils andere achtet. +- [ ] **SW-Update lädt mitten in der Arbeit neu** — beim Test beobachtet: `controllerchange` + → `location.reload()` (index.php) feuerte, während drei Fotos in der Warteschlange + lagen; der Browser zeigte dazu seinen eigenen „Seite verlassen?"-Dialog (`beforeunload`). + War am 17.09. **nicht** die Ursache (kein Deploy, kein `index.php` im Log), trifft aber + jeden, der direkt nach einem Deploy fotografiert. Lösung: Reload aufschieben, solange + ein Modal (Kamera!) offen ist oder die Warteschlange nicht leer ist. + ## Erledigt ### 2026-08-22 diff --git a/app.css b/app.css index e5a3a77..ce8e524 100644 --- a/app.css +++ b/app.css @@ -307,6 +307,37 @@ body { opacity: 0.5; } +/* Wartende Fotos am Auftrag — bewusst auffaellig (gelber Rand): das ist der Zustand, + in dem man sonst glaubt, die Fotos seien weg. */ +.pending-section { + border: 1px solid #e0af68; + background: #2b2820; + cursor: pointer; +} +.pending-section h3 { color: #e0af68; } +.pending-section p { line-height: 1.45; } +.pending-grid .pending-thumb { cursor: pointer; } +.pending-grid .pending-thumb img { opacity: 0.7; } +.pending-grid .pending-mark { + position: absolute; + right: 4px; + bottom: 4px; + font-size: 16px; + line-height: 1; + padding: 3px 4px; + border-radius: 6px; + background: rgba(0, 0, 0, 0.6); +} +.pending-grid .pending-thumb.is-failed { outline: 2px solid #f7768e; outline-offset: -2px; } +.pending-grid .pending-more { + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: 600; + color: #e0af68; +} + /* Foto-Section Kopfzeile mit Auswahl-Toggle */ .photo-section-head { display: flex; diff --git a/app.js b/app.js index 34e384e..5d15b18 100644 --- a/app.js +++ b/app.js @@ -39,21 +39,57 @@ function setBack(visible, hash) { * Jedes Modal pusht beim Öffnen einen eigenen History-Eintrag. * popstate (Android-Back) pop-t den Stack und entfernt das Modal. * Programmatisches Schließen (closeModal) räumt synchron auf und - * setzt ein Skip-Flag, damit der eigene history.back() kein - * doppeltes Cleanup auslöst. + * zählt den eigenen history.back() mit (_pendingBacks), damit dessen + * popstate kein doppeltes Cleanup auslöst. * ============================================================ */ const modalStack = []; -let _skipNextPopstate = false; +// Selbst ausgeloeste history.back(), deren popstate noch aussteht. Zaehler statt Boolean: +// schliessen sich zwei Dialoge direkt nacheinander, stehen ZWEI popstate aus — ein Boolean +// verschluckt nur das erste, das zweite raeumt dann faelschlich den Stapel ab (KB #1209). +let _pendingBacks = 0; +let _backWaiters = []; let _lastBackAt = 0; + +function _flushBackWaiters() { + const w = _backWaiters; + _backWaiters = []; + w.forEach(fn => { try { fn(); } catch (_) {} }); +} + +/* history.back() ist ASYNCHRON. Wer direkt danach den Hash wechselt (closeModal + + * router.go), ueberholt den Ruecksprung: der laeuft erst NACH dem Hash-Wechsel und springt + * hinter den neuen Eintrag zurueck. Auf dem Bildschirm steht dann der Auftrag, in der + * Adresszeile aber '#/orders' — und das naechste router.navigate() (z.B. "Fertig" in der + * Kamera) zeichnet die Auftragsliste. Genau so flog Eddy am 17.09.2026 aus dem frisch + * angelegten Auftrag 111. router.go() wartet deshalb hierauf, bevor es den Hash setzt. + * Die Schutzzeit verhindert, dass ein ausbleibendes popstate die Navigation fuer immer + * blockiert (history.back() am Anfang der History ist ein stilles Nichts). */ +window.historySettled = function () { + if (_pendingBacks <= 0) return Promise.resolve(); + return new Promise(resolve => { + _backWaiters.push(resolve); + setTimeout(() => { + if (_pendingBacks > 0) { _pendingBacks = 0; _flushBackWaiters(); } + }, 1500); + }); +}; // Aktiver Select-Mode-Cleanup: wird von popstate gerufen, damit Android-Back // eine laufende Mehrfachauswahl beendet statt die Seite zu verlassen. let selectModeCleanup = null; function pushModal(el, cleanup) { modalStack.push({ el, cleanup: cleanup || null }); - try { - history.pushState({ _modal: true, _ts: Date.now() }, '', location.hash); - } catch (e) { /* some browsers may block in strict sandboxes */ } + const push = () => { + // Inzwischen schon wieder geschlossen? Dann keinen verwaisten Eintrag anlegen. + if (!modalStack.some(m => m.el === el)) return; + try { + history.pushState({ _modal: true, _ts: Date.now() }, '', location.hash); + } catch (e) { /* some browsers may block in strict sandboxes */ } + }; + // Laeuft noch ein Ruecksprung eines eben geschlossenen Modals (Bestaetigung zu, naechster + // Dialog auf), erst den abwarten — sonst nimmt er den NEUEN Eintrag gleich wieder mit. + if (_pendingBacks > 0) window.historySettled().then(push); + else push(); } function closeModal(el) { @@ -65,12 +101,14 @@ function closeModal(el) { const entry = modalStack[idx]; modalStack.splice(idx, 1); try { entry.el.remove(); } catch {} - try { entry.cleanup && entry.cleanup(); } catch {} - // Eigenen History-Eintrag wieder entfernen, ohne popstate-Recursion + // Eigenen History-Eintrag wieder entfernen, ohne popstate-Recursion. Das passiert VOR + // dem cleanup: navigiert der (Kamera: zurueck in den Auftrag), sieht router.go() den + // ausstehenden Ruecksprung und wartet ihn ab, statt von ihm ueberholt zu werden. if (history.state && history.state._modal) { - _skipNextPopstate = true; + _pendingBacks++; history.back(); } + try { entry.cleanup && entry.cleanup(); } catch {} } function isTopLevelHash(h) { @@ -79,7 +117,11 @@ function isTopLevelHash(h) { } window.addEventListener('popstate', () => { - if (_skipNextPopstate) { _skipNextPopstate = false; return; } + if (_pendingBacks > 0) { + _pendingBacks--; + if (_pendingBacks === 0) _flushBackWaiters(); + return; + } if (modalStack.length > 0) { const top = modalStack.pop(); try { top.el.remove(); } catch {} @@ -790,7 +832,9 @@ router.on('/orders/:id', async (args) => { -
+
+ +

Hochgeladene Fotos (${imagePhotos.length})

${(imagePhotos.length + otherDocs.length) ? '' : ''} @@ -1014,6 +1058,7 @@ router.on('/orders/:id', async (args) => { }); loadThumbs(); + renderPendingPhotos(); // was noch in der Warteschlange liegt, gehoert sichtbar zum Auftrag const camInput = document.getElementById('camera-input'); const galInput = document.getElementById('gallery-input'); @@ -1039,8 +1084,10 @@ router.on('/orders/:id', async (args) => { offline.releaseWakeLock(); } // Nach Upload einfach die Route neu rendern — so werden Select-Mode, - // Click-Handler und Thumbnails sauber neu aufgebaut. - router.navigate(); + // Click-Handler und Thumbnails sauber neu aufgebaut. Ausdruecklich diesen + // Auftrag, nicht den Hash der Adresszeile (siehe Kamera-Cleanup). + router.go('#/orders/' + args.id); + announcePendingPhotos(args.id); } camInput.addEventListener('change', () => handleFiles(camInput.files)); galInput.addEventListener('change', () => handleFiles(galInput.files)); @@ -1049,6 +1096,113 @@ router.on('/orders/:id', async (args) => { } }); +/* ============================================================ + * WARTENDE FOTOS AM AUFTRAG + * + * Die Auftragsseite zeigte nur, was der Server schon hat. Fotos in der Warteschlange + * standen nirgends ausser als Zahl im Badge oben rechts. Wer im Funkloch fotografiert und + * danach in den Auftrag schaut, sieht: nichts — und haelt die Fotos fuer verloren + * (gemeldet 18.09.2026). Deshalb ein eigener Block mit Vorschaubildern direkt aus der + * Warteschlange und einem Satz, der sagt, was los ist. + * + * Der Block haengt an #pending-photos (data-order-id). Den gibt es nur, solange die + * Auftragsseite gezeichnet ist — daran erkennen die Listener unten, ob sie gemeint sind. + * ============================================================ */ +const PENDING_PREVIEW_MAX = 12; // mehr Vorschaubilder kosten nur Speicher +let pendingPhotoUrls = []; +let pendingRenderTimer = null; +let orderRefreshTimer = null; + +async function renderPendingPhotos() { + const box = document.getElementById('pending-photos'); + if (!box) return; + const orderId = box.dataset.orderId; + const items = (await offline.listQueue().catch(() => [])) + .filter(i => i.type === 'photo' && String(i.order_id) === String(orderId)); + if (!document.body.contains(box)) return; // Seite wurde inzwischen neu gezeichnet + + pendingPhotoUrls.forEach(u => { try { URL.revokeObjectURL(u); } catch (_) {} }); + pendingPhotoUrls = []; + if (!items.length) { box.innerHTML = ''; return; } + + const failed = items.filter(i => i.failed).length; + const waiting = items.length - failed; + const fotos = (n) => n + (n === 1 ? ' Foto' : ' Fotos'); + const lines = []; + if (waiting) { + lines.push(navigator.onLine === false + ? '📴 Kein Netz — ' + fotos(waiting) + (waiting === 1 ? ' ist' : ' sind') + + ' auf dem Gerät gesichert und ' + (waiting === 1 ? 'wird' : 'werden') + + ' gesendet, sobald wieder Netz da ist.' + : fotos(waiting) + (waiting === 1 ? ' ist' : ' sind') + ' auf dem Gerät gesichert und ' + + (waiting === 1 ? 'geht' : 'gehen') + ' raus, sobald die Verbindung steht.'); + } + if (failed) lines.push('⚠️ ' + fotos(failed) + ' konnte' + (failed === 1 ? '' : 'n') + + ' nicht hochgeladen werden — antippen zum Prüfen.'); + + const shown = items.slice(0, PENDING_PREVIEW_MAX); + const thumbs = shown.map(it => { + const url = URL.createObjectURL(new Blob([it.data], { type: it.mime || 'image/jpeg' })); + pendingPhotoUrls.push(url); + return '
' + + '' + + '' + (it.failed ? '⚠️' : '⏳') + '
'; + }).join(''); + const more = items.length - shown.length; + + box.innerHTML = '
' + + '

⏳ Warten auf Upload (' + items.length + ')

' + + lines.map(l => '

' + escapeHtml(l) + '

').join('') + + '
' + thumbs + + (more > 0 ? '
+' + more + '
' : '') + + '
'; + box.querySelector('.pending-section').onclick = () => openUploadQueueModal(); +} + +/* Nach dem Schliessen der Kamera sagen, was mit den Fotos ist. Ohne den Satz sieht + * "gesichert, aber noch nicht gesendet" genauso aus wie "alles erledigt". */ +async function announcePendingPhotos(orderId) { + const n = (await offline.listQueue().catch(() => [])) + .filter(i => i.type === 'photo' && String(i.order_id) === String(orderId)).length; + if (!n) return; + const fotos = n + (n === 1 ? ' Foto' : ' Fotos'); + showToast(navigator.onLine === false + ? '📴 Kein Netz — ' + fotos + ' auf dem Gerät gesichert, Upload folgt automatisch' + : '⏳ ' + fotos + (n === 1 ? ' wartet' : ' warten') + ' noch auf den Upload', 'warn'); +} + +function renderPendingPhotosSoon() { + if (!document.getElementById('pending-photos')) return; + clearTimeout(pendingRenderTimer); + pendingRenderTimer = setTimeout(renderPendingPhotos, 300); +} + +/* Kam ein Foto dieses Auftrags an, die Seite nachziehen — sonst steht es weder unter + * "Warten auf Upload" noch unter "Hochgeladene Fotos". Gebuendelt (eine Serie soll nicht + * je Foto neu zeichnen) und nur, wenn gerade nichts offen ist, das dabei kaputtginge. */ +function scheduleOrderRefresh() { + clearTimeout(orderRefreshTimer); + orderRefreshTimer = setTimeout(async () => { + const box = document.getElementById('pending-photos'); + if (!box) return; // Auftragsseite nicht mehr offen + const playing = Array.from(document.querySelectorAll('#main audio')).some(a => !a.paused); + if (modalStack.length || selectModeCleanup || playing) { scheduleOrderRefresh(); return; } + const y = window.scrollY, my = main().scrollTop; + try { await router.navigate('#/orders/' + box.dataset.orderId); } catch (_) {} + try { window.scrollTo(0, y); main().scrollTop = my; } catch (_) {} + }, 2500); +} + +window.addEventListener('photo-uploaded', (e) => { + const box = document.getElementById('pending-photos'); + if (!box || String((e.detail || {}).orderId) !== String(box.dataset.orderId)) return; + renderPendingPhotosSoon(); + scheduleOrderRefresh(); +}); +window.addEventListener('queue-changed', renderPendingPhotosSoon); +window.addEventListener('online', renderPendingPhotosSoon); +window.addEventListener('offline', renderPendingPhotosSoon); + /* Fotoeinstellungen aus dem Bericht-Modul (Admin → Fotoqualität). * Vorgabe ist Originalgroesse: 0 = gar nicht verkleinern. Der Wert wird lokal * zwischengespeichert, damit die Kamera auch offline sofort weiss, was gilt. @@ -3349,8 +3503,14 @@ async function openCameraModal(orderId) { stopStream(); window.removeEventListener('photo-uploaded', onUploaded); strip.querySelectorAll('img').forEach(img => { try { URL.revokeObjectURL(img.src); } catch (_) {} }); - // Auftragsseite nur neu laden, wenn wirklich Fotos aufgenommen wurden (sonst unnötiger Roundtrip) - if (shots > 0) { try { router.navigate(); } catch (_) {} } + // Auftragsseite nur neu laden, wenn wirklich Fotos aufgenommen wurden (sonst unnötiger Roundtrip). + // Ausdruecklich DIESEN Auftrag ansteuern, nicht "was gerade in der Adresszeile steht": + // stand dort nach dem History-Race '#/orders', zeichnete router.navigate() die + // Auftragsliste und man flog nach "Fertig" aus dem Auftrag (17.09.2026). + if (shots > 0) { + try { router.go('#/orders/' + orderId); } catch (_) {} + announcePendingPhotos(orderId); + } }); modal.querySelector('#cam-close').onclick = () => closeModal(modal); diff --git a/lib/router.js b/lib/router.js index 3d49344..863780c 100644 --- a/lib/router.js +++ b/lib/router.js @@ -45,11 +45,18 @@ } function go(hash) { - if (hash !== window.location.hash) { - window.location.hash = hash; - } else { - navigate(hash); - } + // Erst ausstehende history.back() (geschlossene Modals) durchlaufen lassen. Ein + // Hash-Wechsel davor wird vom Ruecksprung ueberholt, die Adresse steht danach auf + // der ALTEN Route, waehrend die neue angezeigt wird (app.js: historySettled). + const settled = (typeof window.historySettled === 'function') + ? window.historySettled() : Promise.resolve(); + settled.then(() => { + if (hash !== window.location.hash) { + window.location.hash = hash; + } else { + navigate(hash); + } + }); } window.addEventListener('hashchange', () => navigate());