baustelle-pwa/lib/idb.js
Eduard Wisch a108664854 App ohne Netz lesbar statt "Failed to fetch"
Gemeldet: die App zeigte beim Oeffnen nur "Failed to fetch", waehrend das
Teilen aus WhatsApp weiter ging. Deploy und API waren in Ordnung — die
Meldung ist die rohe Browserausgabe eines fetch(), das gar nicht bis zum
Server kam, und stand 1:1 im UI, weil die Routen im catch e.message
anzeigten. Das Teilen fiel nicht auf, weil share.html sich die letzten
Auftraege seit eaad796 lokal merkt; die Hauptapp hatte fuer die Listen
keinen Offline-Fallback.

- api.js uebersetzt Netzfehler in einen eigenen Fehler mit offline=true
  und deutschem Text; 4xx/5xx bleiben Serverfehler
- isTransient() in offline.js erkannte Netzfehler an "instanceof TypeError".
  Ohne Anpassung waere ein Foto aus dem Funkloch als dauerhaft
  fehlgeschlagen gewertet und nach sechs Versuchen in Quarantaene gelandet
- Datenspiegel in IndexedDB: Auftrags-, Kunden- und Berichtsliste sowie
  geoeffnete Details (letzte 30). Ohne Netz wird der Stand angezeigt,
  mit Band "Kein Netz — Stand von HH:MM Uhr", und die Suche filtert darin
- beim Abmelden wird der Spiegel geloescht (geteiltes Geraet)
- sw.js: der Same-Origin-Zweig endete auf caches.match(); fand der Cache
  nichts, wurde respondWith(undefined) aufgerufen und die Anfrage
  scheiterte erneut mit genau der Meldung, die verhindert werden sollte.
  Jetzt immer eine echte Response, nur vollstaendige Antworten im Cache
- Precache nach Deploy: die Seite meldet dem Worker ihre echten Asset-URLs,
  sonst ist der Cache direkt nach einem Deploy leer (?v=<mtime>)

Muster aus KB #1037 und #1041. Verifiziert auf der lokalen Testinstanz
inklusive echtem Funkloch (setOffline + reload): App startet aus dem
Cache, zeigt Band und Auftragsliste, keine JS-Fehler.
2026-08-31 19:07:09 +02:00

111 lines
3.8 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);
});
}
window.idb = { get, set, del, keys, queuePush, queueAll, queueDelete, queueUpdate };
})();