baustelle-pwa/lib/idb.js
Eddy 73a481fa8f
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 13s
Videos aufnehmen, Emulator-Prüfungen nachgeholt, keine Versalien [deploy]
Eddy: „Bei Fotos aufnehmen auch Videos unterstützen bitte".

- Kamera: Umschalter Foto | Video, roter Aufnahmeknopf, Laufzeit mit
  Höchstlänge (5 min bzw. passend zur Servergrenze), Ton, Licht/Zoom/
  Antippen-Fokus auch beim Filmen; MP4 (H.264/AAC), sonst WebM.
  Schließen oder App-Wechsel während der Aufnahme sichert das Video.
- Warteschlange: Videos wie Fotos (Persist-First). Gesichert wird der
  Blob statt eines ArrayBuffers; Zeitlimit und Lease wachsen mit der
  Größe (api.js/sw.js gleiche Formel); 413 → sofort Quarantäne.
- Auftrag: Abschnitt „Videos" mit erstem Bild, Player (spult per Range),
  Teilen, Löschen; „Warten auf Upload" zeigt Videos mit.
- Galerie und Teilen nehmen Videos, zu große lehnt die App gleich ab.
- Überschriften fett in normaler Schreibweise statt Versalien.
- ROADMAP: Funkloch, Display aus, 10 Fotos aus der Galerie und
  PDF-Zoom im Emulator geprüft und abgehakt.

Braucht Bericht 1.8.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:54:20 +02:00

187 lines
7.3 KiB
JavaScript

/* 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 bei aelteren Eintraegen (ArrayBuffer) jedes Foto komplett in den Speicher; neue
// Eintraege tragen einen Blob, den der Browser erst beim Lesen der Daten holt.
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.
* leaseMs: Zahl oder Funktion(eintrag) — ein Video braucht laenger als ein Foto. Die Dauer
* steht im Eintrag (lease_ms), damit der andere weiss, wie lange er warten muss.
* Rueckgabe: der belegte Eintrag, oder null (weg, fehlgeschlagen-markiert oder belegt). */
function queueClaim(id, owner, leaseMs) {
const now = Date.now();
return queuePatch(id, (it) => {
const lease = typeof leaseMs === 'function' ? leaseMs(it) : leaseMs;
if (it.failed) return false;
if (it.uploading_since && it.uploading_by !== owner
&& (now - it.uploading_since) < (it.lease_ms || lease)) return false;
it.uploading_since = now;
it.uploading_by = owner;
it.lease_ms = lease;
});
}
function queueRelease(id, extra) {
return queuePatch(id, (it) => {
delete it.uploading_since;
delete it.uploading_by;
delete it.lease_ms;
if (extra) extra(it);
});
}
// Groesse der gesicherten Daten — aeltere Eintraege tragen einen ArrayBuffer, neue einen Blob
function dataSize(it) {
const d = it && it.data;
if (!d) return 0;
return typeof d.size === 'number' ? d.size : (d.byteLength || 0);
}
window.idb = { get, set, del, keys, queuePush, queueAll, queueDelete, queueUpdate,
queueKeys, queuePatch, queueClaim, queueRelease, dataSize };
})();