/* Mini IndexedDB Wrapper für die PWA. * Speichert: Auth-Token, Offline-Queue für Uploads. */ (function () { const DB_NAME = 'baustelle-pwa-v1'; const DB_VERSION = 1; let dbPromise = null; function open() { if (dbPromise) return dbPromise; dbPromise = new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = () => { const db = req.result; if (!db.objectStoreNames.contains('kv')) { db.createObjectStore('kv'); } if (!db.objectStoreNames.contains('queue')) { db.createObjectStore('queue', { keyPath: 'id', autoIncrement: true }); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); return dbPromise; } async function get(key) { const db = await open(); return new Promise((res, rej) => { const tx = db.transaction('kv', 'readonly'); const r = tx.objectStore('kv').get(key); r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); }); } async function set(key, val) { const db = await open(); return new Promise((res, rej) => { const tx = db.transaction('kv', 'readwrite'); tx.objectStore('kv').put(val, key); tx.oncomplete = () => res(); tx.onerror = () => rej(tx.error); }); } async function del(key) { const db = await open(); return new Promise((res, rej) => { const tx = db.transaction('kv', 'readwrite'); tx.objectStore('kv').delete(key); tx.oncomplete = () => res(); tx.onerror = () => rej(tx.error); }); } // Alle Schluessel im kv-Store — gebraucht, um den Offline-Spiegel aufzuraeumen async function keys() { const db = await open(); return new Promise((res, rej) => { const tx = db.transaction('kv', 'readonly'); const r = tx.objectStore('kv').getAllKeys(); r.onsuccess = () => res(r.result || []); r.onerror = () => rej(r.error); }); } async function queuePush(item) { const db = await open(); return new Promise((res, rej) => { const tx = db.transaction('queue', 'readwrite'); const r = tx.objectStore('queue').add(item); r.onsuccess = () => res(r.result); r.onerror = () => rej(r.error); }); } async function queueAll() { const db = await open(); return new Promise((res, rej) => { const tx = db.transaction('queue', 'readonly'); const r = tx.objectStore('queue').getAll(); r.onsuccess = () => res(r.result || []); r.onerror = () => rej(r.error); }); } async function queueDelete(id) { const db = await open(); return new Promise((res, rej) => { const tx = db.transaction('queue', 'readwrite'); tx.objectStore('queue').delete(id); tx.oncomplete = () => res(); tx.onerror = () => rej(tx.error); }); } // Item aktualisieren (z.B. Versuchszähler / Fehler-Markierung) — put upsert-t per keyPath id async function queueUpdate(item) { const db = await open(); return new Promise((res, rej) => { const tx = db.transaction('queue', 'readwrite'); tx.objectStore('queue').put(item); tx.oncomplete = () => res(); tx.onerror = () => rej(tx.error); }); } // Nur die Ids — wer die Queue abarbeitet, holt die Eintraege danach einzeln. queueAll() // zieht dagegen jedes Foto komplett in den Speicher. async function queueKeys() { const db = await open(); return new Promise((res, rej) => { const tx = db.transaction('queue', 'readonly'); const r = tx.objectStore('queue').getAllKeys(); r.onsuccess = () => res(r.result || []); r.onerror = () => rej(r.error); }); } /* Lesen-Aendern-Schreiben in EINER Transaktion. IndexedDB reiht readwrite-Transaktionen * auf demselben Store ueber alle Kontexte hinweg hintereinander — Seite und Service * Worker koennen sich hier also nicht ins Wort fallen. `fn(item)` aendert den Eintrag an * Ort und Stelle; gibt sie `false` zurueck, wird NICHT geschrieben. * Rueckgabe: der Eintrag nach der Aenderung, oder null (weg bzw. nicht geschrieben). */ async function queuePatch(id, fn) { const db = await open(); return new Promise((res, rej) => { const tx = db.transaction('queue', 'readwrite'); const store = tx.objectStore('queue'); let out = null; const r = store.get(id); r.onsuccess = () => { const it = r.result; if (!it) return; // inzwischen hochgeladen und geloescht if (fn(it) === false) return; store.put(it); out = it; }; tx.oncomplete = () => res(out); tx.onerror = () => rej(tx.error); tx.onabort = () => rej(tx.error); }); } /* Einen Eintrag fuer den Upload belegen (Lease). Seite und Service Worker arbeiten * dieselbe Queue ab; ohne Absprache laden beide dasselbe Foto hoch (Prod-Log 17.09.2026: * reihenweise duplicate:true). Belegt wird nur, was frei ist oder dessen Lease abgelaufen * ist — eine eingefrorene oder abgeschossene Seite blockiert ihr Foto also nicht ewig. * DIESELBE LOGIK STEHT IN sw.js (queueClaim) — beide Stellen zusammen aendern. * Rueckgabe: der belegte Eintrag, oder null (weg, fehlgeschlagen-markiert oder belegt). */ function queueClaim(id, owner, leaseMs) { const now = Date.now(); return queuePatch(id, (it) => { if (it.failed) return false; if (it.uploading_since && it.uploading_by !== owner && (now - it.uploading_since) < leaseMs) return false; it.uploading_since = now; it.uploading_by = owner; }); } function queueRelease(id, extra) { return queuePatch(id, (it) => { delete it.uploading_since; delete it.uploading_by; if (extra) extra(it); }); } window.idb = { get, set, del, keys, queuePush, queueAll, queueDelete, queueUpdate, queueKeys, queuePatch, queueClaim, queueRelease }; })();