All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 13s
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>
187 lines
7.3 KiB
JavaScript
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 };
|
|
})();
|