baustelle-pwa/lib/router.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

64 lines
2.1 KiB
JavaScript

/* Mini Hash-Router für die PWA.
* Routen:
* #/login
* #/orders
* #/orders/<id>
* #/orders/<id>/upload
* #/settings
*/
(function () {
const routes = {};
let currentRoute = null;
function on(pattern, handler) {
// Pattern: '/orders/:id'
const re = new RegExp('^' + pattern.replace(/:[a-z]+/gi, '([^/]+)') + '$');
const params = (pattern.match(/:([a-z]+)/gi) || []).map(s => s.slice(1));
routes[pattern] = { re, params, handler };
}
async function navigate(hash) {
if (!hash) hash = window.location.hash || '#/orders';
const path = hash.replace(/^#/, '') || '/orders';
for (const key of Object.keys(routes)) {
const r = routes[key];
const m = path.match(r.re);
if (m) {
const args = {};
r.params.forEach((p, i) => { args[p] = decodeURIComponent(m[i + 1]); });
currentRoute = { pattern: key, args };
// Das Offline-Band gilt immer nur fuer die gerade gezeichnete Ansicht.
// Ohne dieses Zuruecksetzen bliebe es auf Seiten stehen, die gar keinen
// Spiegel haben (Einstellungen, Lieferungen).
if (typeof window.setStale === 'function') window.setStale(false);
try {
await r.handler(args);
} catch (e) {
console.error(e);
showToast('Fehler: ' + e.message, 'error');
}
return;
}
}
// Default
navigate('#/orders');
}
function go(hash) {
if (hash !== window.location.hash) {
window.location.hash = hash;
} else {
navigate(hash);
}
}
window.addEventListener('hashchange', () => navigate());
document.addEventListener('DOMContentLoaded', async () => {
if (typeof window.appBoot === 'function') {
try { await window.appBoot(); } catch (e) {}
}
navigate();
});
window.router = { on, navigate, go };
})();