baustelle-pwa/sw.js
Eduard Wisch 02ffdca57c
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 1s
feat: Phase 4 Block 1 — Seite bearbeiten, PDF-Vorschau, Unterschrift, Share Target
Bericht-Detail-Ansicht:
- Tap auf Seiten-Thumb → openPageActionsModal mit:
  - Vorschaubild
  - Textarea für Seiten-Notiz (wird im PDF unter der Seite gedruckt)
  - '💾 Notiz speichern' → api.updatePageNote
  - 🗑️ im Header → api.deletePage mit Confirm
- Seiten-Thumbs haben Nummer-Badge oben links (1, 2, 3…)
- Neuer '👁 PDF-Vorschau'-Button öffnet openPdfModal (iframe mit
  Blob-URL) für Final-PDF oder on-the-fly Preview
- Neuer '✍️ Kunden-Unterschrift hinzufügen'-Button öffnet
  openSignatureModal: Touch-Canvas 2:1, Clear, Save → PNG wird als
  neue Bericht-Seite mit note='Unterschrift Kunde' angelegt

Web Share Target API:
- manifest.webmanifest: share_target mit photos array
- share.html: empfängt geteilte Fotos aus IDB (vom SW befüllt),
  zeigt Auftragsliste, Tap → Upload aller Fotos
- Service Worker v5: fängt POST /share.html ab, schreibt Files in
  IDB Key 'shared_files', redirected 303

Cache-Version bumpt damit neue Files geladen werden.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
2026-04-09 01:00:24 +02:00

94 lines
3.1 KiB
JavaScript

/* Baustelle PWA Service Worker.
* Cache-Strategie:
* - App-Shell (HTML/CSS/JS): cache-first, network update
* - API-Calls: network-first, kein offline-cache (da auth-pflichtig)
*/
const CACHE = 'baustelle-v5';
const SHELL = [
'./',
'./index.html',
'./share.html',
'./app.css',
'./app.js',
'./manifest.webmanifest',
'./lib/idb.js',
'./lib/api.js',
'./lib/offline.js',
'./lib/router.js',
'./icons/icon-192.png',
'./icons/icon-512.png',
];
// Web Share Target: eingehende POSTs an share.html abfangen und in IDB zwischenspeichern
async function handleShareTarget(request) {
const fd = await request.formData();
const files = fd.getAll('photos');
if (files.length) {
const db = await new Promise((res, rej) => {
const req = indexedDB.open('baustelle-pwa-v1', 1);
req.onupgradeneeded = () => {
const d = req.result;
if (!d.objectStoreNames.contains('kv')) d.createObjectStore('kv');
if (!d.objectStoreNames.contains('queue')) d.createObjectStore('queue', { keyPath: 'id', autoIncrement: true });
};
req.onsuccess = () => res(req.result);
req.onerror = () => rej(req.error);
});
const tx = db.transaction('kv', 'readwrite');
tx.objectStore('kv').put(files.map(f => ({ name: f.name, type: f.type, data: f })), 'shared_files');
await new Promise(res => tx.oncomplete = res);
}
return Response.redirect('./share.html', 303);
}
self.addEventListener('install', (e) => {
e.waitUntil(caches.open(CACHE).then(c => c.addAll(SHELL).catch(() => null)));
self.skipWaiting();
});
self.addEventListener('activate', (e) => {
e.waitUntil(
caches.keys().then(keys => Promise.all(
keys.filter(k => k !== CACHE).map(k => caches.delete(k))
))
);
self.clients.claim();
});
self.addEventListener('fetch', (e) => {
const url = new URL(e.request.url);
// Web Share Target: POST auf share.html abfangen
if (e.request.method === 'POST' && url.pathname.endsWith('/share.html')) {
e.respondWith(handleShareTarget(e.request));
return;
}
// API-Requests: nicht cachen, durchreichen
if (url.pathname.includes('/custom/bericht/api/')) {
return; // default network
}
// App-Shell: cache-first
if (e.request.method === 'GET' && url.origin === location.origin) {
e.respondWith(
caches.match(e.request).then(hit => {
if (hit) {
// Background-Update
fetch(e.request).then(r => {
if (r.ok) caches.open(CACHE).then(c => c.put(e.request, r.clone()));
}).catch(() => null);
return hit;
}
return fetch(e.request).then(r => {
if (r.ok) {
const clone = r.clone();
caches.open(CACHE).then(c => c.put(e.request, clone));
}
return r;
});
})
);
}
});