PRIORITY_PAGES erweitert auf 10 Seiten (war 8): zusätzlich health.js, notes.js, expenses.js. admin.js raus — 233 KB, offline irrelevant. Damit funktionieren offline ohne vorherigen Besuch: Tagebuch · Gesundheit · Karte · Gassi · Erste Hilfe · Notizblock Ausgaben · Routen · Giftköder · Vermisst. Offline-Indikator Step 2 prüft jetzt alle 7 vom User genannten Seiten (diary, map, walks, erste-hilfe, notes, expenses, routes) — Pfote wird grün wenn alle im Static-Cache sind. CSS-Färbung umgestellt: nur stroke (Linie) wird grün, kein fill mehr. Pfote behält ihre offene Optik, nur die Outlines wechseln von weiß zu Grün.
575 lines
20 KiB
JavaScript
575 lines
20 KiB
JavaScript
/* ============================================================
|
|
BAN YARO — Service Worker
|
|
Offline-Cache + Push Notifications + Tile-Cache
|
|
============================================================ */
|
|
|
|
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
|
const VER = '1085';
|
|
const CACHE_VERSION = `by-v${VER}`;
|
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
|
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
|
|
|
// Prioritäts-Seiten: werden nach Install im Hintergrund gecacht (nicht blockierend)
|
|
// Diese Seiten MÜSSEN offline funktionieren — auch wenn der User sie noch nie geöffnet hat.
|
|
const PRIORITY_PAGES = [
|
|
'/js/pages/diary.js',
|
|
'/js/pages/health.js',
|
|
'/js/pages/map.js',
|
|
'/js/pages/walks.js',
|
|
'/js/pages/erste-hilfe.js',
|
|
'/js/pages/notes.js',
|
|
'/js/pages/expenses.js',
|
|
'/js/pages/routes.js',
|
|
'/js/pages/poison.js',
|
|
'/js/pages/lost.js',
|
|
];
|
|
|
|
// index.html wird NICHT pre-gecacht (immer Network-First)
|
|
const STATIC_ASSETS = [
|
|
`/css/design-system.css?v=${VER}`,
|
|
`/css/layout.css?v=${VER}`,
|
|
`/css/components.css?v=${VER}`,
|
|
'/icons/phosphor.svg',
|
|
`/js/api.js?v=${VER}`,
|
|
`/js/ui.js?v=${VER}`,
|
|
`/js/app.js?v=${VER}`,
|
|
`/js/worlds.js?v=${VER}`,
|
|
`/js/offline-indicator.js?v=${VER}`,
|
|
'/js/leaflet.markercluster.js',
|
|
'/css/MarkerCluster.css',
|
|
'/css/MarkerCluster.Default.css',
|
|
'/manifest.json',
|
|
'/icons/icon-192.png',
|
|
];
|
|
|
|
// ----------------------------------------------------------
|
|
// HILFSFUNKTIONEN
|
|
// ----------------------------------------------------------
|
|
|
|
// Fetch mit Timeout (verhindert Hängen bei schlechtem Netz)
|
|
function _fetchTimeout(request, ms = 8000) {
|
|
const ctrl = new AbortController();
|
|
const tid = setTimeout(() => ctrl.abort(), ms);
|
|
return fetch(request, { signal: ctrl.signal }).finally(() => clearTimeout(tid));
|
|
}
|
|
|
|
// IndexedDB Write-Queue (in SW-Kontext verfügbar)
|
|
function _queueDb() {
|
|
return new Promise((res, rej) => {
|
|
const req = indexedDB.open('by-offline-queue', 1);
|
|
req.onupgradeneeded = e => {
|
|
const db = e.target.result;
|
|
if (!db.objectStoreNames.contains('q'))
|
|
db.createObjectStore('q', { keyPath: 'id', autoIncrement: true });
|
|
};
|
|
req.onsuccess = e => res(e.target.result);
|
|
req.onerror = e => rej(e.target.error);
|
|
});
|
|
}
|
|
async function _queueAdd(entry) {
|
|
const db = await _queueDb();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('q', 'readwrite');
|
|
const r = tx.objectStore('q').add(entry);
|
|
r.onsuccess = () => res(r.result);
|
|
r.onerror = () => rej(r.error);
|
|
});
|
|
}
|
|
async function _queueAll() {
|
|
const db = await _queueDb();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('q', 'readonly');
|
|
const r = tx.objectStore('q').getAll();
|
|
r.onsuccess = () => res(r.result);
|
|
r.onerror = () => rej(r.error);
|
|
});
|
|
}
|
|
async function _queueDel(id) {
|
|
const db = await _queueDb();
|
|
return new Promise((res, rej) => {
|
|
const tx = db.transaction('q', 'readwrite');
|
|
const r = tx.objectStore('q').delete(id);
|
|
r.onsuccess = () => res();
|
|
r.onerror = () => rej(r.error);
|
|
});
|
|
}
|
|
|
|
// Queue abarbeiten (bei Background-Sync oder manuellem Trigger)
|
|
async function _processQueue() {
|
|
const items = await _queueAll();
|
|
let synced = 0, failed = 0;
|
|
for (const entry of items) {
|
|
try {
|
|
const resp = await fetch(entry.url, {
|
|
method: entry.method,
|
|
headers: entry.headers,
|
|
body: entry.body || undefined,
|
|
});
|
|
// 2xx und 4xx aus Queue entfernen (4xx = Userfehler, nicht Netzwerk)
|
|
if (resp.status < 500) { await _queueDel(entry.id); synced++; }
|
|
else { failed++; }
|
|
} catch { failed++; }
|
|
}
|
|
// Clients benachrichtigen
|
|
const clients = await self.clients.matchAll({ includeUncontrolled: true });
|
|
clients.forEach(c => c.postMessage({ type: 'QUEUE_PROCESSED', synced, failed, total: items.length }));
|
|
return { synced, failed };
|
|
}
|
|
|
|
// Kann dieser Endpunkt offline in die Queue?
|
|
const _QUEUEABLE = [
|
|
{ re: /^\/api\/dogs\/\d+\/diary$/, methods: ['POST'] },
|
|
{ re: /^\/api\/dogs\/\d+\/diary\/\d+$/, methods: ['PATCH'] },
|
|
{ re: /^\/api\/dogs\/\d+\/health$/, methods: ['POST'] },
|
|
{ re: /^\/api\/dogs\/\d+\/diary\/\d+\/media$/, methods: ['POST'], skipBody: true },
|
|
{ re: /^\/api\/training\/sessions$/, methods: ['POST'] },
|
|
{ re: /^\/api\/training\/progress$/, methods: ['POST'] },
|
|
{ re: /^\/api\/poison$/, methods: ['POST'] },
|
|
{ re: /^\/api\/lost\/report$/, methods: ['POST'] },
|
|
{ re: /^\/api\/walks$/, methods: ['POST'] },
|
|
];
|
|
function _isQueueable(pathname, method) {
|
|
return _QUEUEABLE.some(q => q.methods.includes(method) && q.re.test(pathname));
|
|
}
|
|
function _isMediaUpload(request) {
|
|
return (request.headers.get('Content-Type') || '').includes('multipart');
|
|
}
|
|
|
|
// Tile-Cache LRU: Eviction wenn zu viele Tiles drin sind
|
|
// cache.keys() liefert Insertion-Order — daher löschen wir vom Anfang.
|
|
// Bei jedem Tile-Add: trimTileCache() im Hintergrund (kein await vor respondWith).
|
|
const _TILE_MAX_ENTRIES = 500;
|
|
let _tileTrimRunning = false;
|
|
async function trimTileCache(maxEntries = _TILE_MAX_ENTRIES) {
|
|
if (_tileTrimRunning) return; // gleichzeitige Trims verhindern
|
|
_tileTrimRunning = true;
|
|
try {
|
|
const cache = await caches.open(CACHE_TILES);
|
|
const keys = await cache.keys();
|
|
if (keys.length > maxEntries) {
|
|
const toDelete = keys.slice(0, keys.length - maxEntries);
|
|
await Promise.all(toDelete.map(k => cache.delete(k)));
|
|
}
|
|
} catch {} finally {
|
|
_tileTrimRunning = false;
|
|
}
|
|
}
|
|
|
|
// Welche GET-API-Endpoints sollen gecacht werden?
|
|
const _CACHEABLE_GET = [
|
|
/^\/api\/dogs(\/\d+)?$/,
|
|
/^\/api\/dogs\/\d+\/welcome-dashboard/,
|
|
/^\/api\/dogs\/\d+\/diary/,
|
|
/^\/api\/dogs\/\d+\/health(?!\/ki-)/, // ki-berichte + ki-zusammenfassung nie cachen
|
|
/^\/api\/training\/exercises/,
|
|
/^\/api\/training\/progress/,
|
|
/^\/api\/training\/plan-progress/,
|
|
/^\/api\/wiki\/rassen/,
|
|
/^\/api\/dogs\/\d+\/diary\/stats/,
|
|
/^\/api\/routes/,
|
|
/^\/api\/places/,
|
|
/^\/api\/breeder\/map-markers/,
|
|
/^\/api\/gassi-zeiten/,
|
|
/^\/api\/poison/,
|
|
/^\/api\/walks/,
|
|
/^\/api\/lost/,
|
|
/^\/api\/expenses/,
|
|
// Drei Welten — offline-fähig
|
|
/^\/api\/streak\/\d+/,
|
|
/^\/api\/forum\/threads/,
|
|
/^\/api\/weather$/,
|
|
/^\/api\/passport\/\d+$/,
|
|
];
|
|
function _isCacheableGet(pathname) {
|
|
return _CACHEABLE_GET.some(re => re.test(pathname));
|
|
}
|
|
|
|
// Cache-TTL: stabile Daten länger, dynamische kürzer
|
|
const _STABLE_GET = [/^\/api\/training\/exercises/, /^\/api\/wiki\/rassen/];
|
|
const _SHORT_GET = [/^\/api\/training\/progress/, /^\/api\/training\/plan-progress/];
|
|
const _TTL_STABLE = 60 * 60 * 1000; // 1 Stunde
|
|
const _TTL_SHORT = 30 * 1000; // 30 Sekunden (Fortschritte, hund-spezifisch)
|
|
const _TTL_DEFAULT = 5 * 60 * 1000; // 5 Minuten
|
|
|
|
const _cacheTs = new Map(); // pathname → timestamp (in-memory, ok bei SW-Neustart)
|
|
|
|
function _cacheTTL(pathname) {
|
|
if (_STABLE_GET.some(re => re.test(pathname))) return _TTL_STABLE;
|
|
if (_SHORT_GET.some(re => re.test(pathname))) return _TTL_SHORT;
|
|
return _TTL_DEFAULT;
|
|
}
|
|
function _cacheStale(pathname) {
|
|
const ts = _cacheTs.get(pathname);
|
|
return !ts || (Date.now() - ts) > _cacheTTL(pathname);
|
|
}
|
|
function _cacheMark(pathname) {
|
|
_cacheTs.set(pathname, Date.now());
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// INSTALL — App Shell cachen
|
|
// ----------------------------------------------------------
|
|
self.addEventListener('install', event => {
|
|
self.skipWaiting();
|
|
event.waitUntil(
|
|
caches.open(CACHE_STATIC)
|
|
.then(cache => cache.addAll(STATIC_ASSETS))
|
|
.then(() => {
|
|
// Prioritäts-Seiten nicht-blockierend im Hintergrund cachen
|
|
caches.open(CACHE_STATIC).then(cache => {
|
|
PRIORITY_PAGES.forEach(page =>
|
|
fetch(page).then(r => { if (r.ok) cache.put(page, r.clone()); }).catch(() => {})
|
|
);
|
|
});
|
|
// Training-Exercises vorwärmen
|
|
return caches.open(CACHE_API).then(c =>
|
|
fetch('/api/training/exercises').then(r => { if (r.ok) c.put('/api/training/exercises', r); }).catch(() => {})
|
|
);
|
|
})
|
|
);
|
|
});
|
|
|
|
// ----------------------------------------------------------
|
|
// ACTIVATE — alte Caches aufräumen (CACHE_TILES + CACHE_API behalten)
|
|
// ----------------------------------------------------------
|
|
self.addEventListener('activate', event => {
|
|
event.waitUntil(
|
|
caches.keys()
|
|
.then(keys => Promise.all(
|
|
keys
|
|
.filter(k => k !== CACHE_STATIC && k !== CACHE_TILES && k !== CACHE_API)
|
|
.map(k => caches.delete(k))
|
|
))
|
|
.then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
// ----------------------------------------------------------
|
|
// FETCH — Network-First für HTML, Cache-First für Assets, API mit Offline-Logik
|
|
// ----------------------------------------------------------
|
|
self.addEventListener('fetch', event => {
|
|
const url = new URL(event.request.url);
|
|
|
|
// Force-Update: SW komplett umgehen → direkt vom Netzwerk
|
|
if (url.searchParams.has('_nocache')) {
|
|
event.respondWith(fetch(event.request.url.replace(/[?&]_nocache=[^&]*/,'') || '/', { cache: 'no-store' }));
|
|
return;
|
|
}
|
|
|
|
// API-Calls mit Timeout, Caching und Write-Queue
|
|
if (url.pathname.startsWith('/api/')) {
|
|
const method = event.request.method;
|
|
|
|
// GET: Network-First mit Timeout, Cache-Fallback, im Hintergrund aktualisieren
|
|
if (method === 'GET' && _isCacheableGet(url.pathname)) {
|
|
event.respondWith((async () => {
|
|
const cached = await caches.match(event.request);
|
|
const stale = _cacheStale(url.pathname);
|
|
|
|
const networkPromise = _fetchTimeout(event.request.clone(), 8000)
|
|
.then(resp => {
|
|
if (resp.ok) {
|
|
_cacheMark(url.pathname);
|
|
const toCache = resp.clone();
|
|
caches.open(CACHE_API).then(c => c.put(event.request, toCache));
|
|
}
|
|
return resp;
|
|
})
|
|
.catch(() => null);
|
|
|
|
// Cache noch frisch → sofort zurückgeben, Netz im Hintergrund
|
|
if (cached && !stale) {
|
|
networkPromise.catch(() => {});
|
|
return cached;
|
|
}
|
|
// Cache vorhanden aber abgelaufen → Netz zuerst, Cache als Fallback
|
|
const fresh = await networkPromise;
|
|
if (fresh) return fresh;
|
|
if (cached) return cached; // lieber veraltet als nichts
|
|
return new Response(JSON.stringify({ detail: 'Offline — keine Daten im Cache.' }),
|
|
{ status: 503, headers: { 'Content-Type': 'application/json' } });
|
|
})());
|
|
return;
|
|
}
|
|
|
|
// Media-Uploads + KI-Endpoints: direkt ans Netzwerk — kein Clone, kein Timeout, kein Queue
|
|
// KI-Anfragen ans lokale LLM können mehrere Minuten dauern
|
|
if (method === 'POST' && (_isMediaUpload(event.request) || url.pathname.startsWith('/api/ki/') || url.pathname.includes('/health/ki-') || url.pathname.includes('/health/symptom'))) return;
|
|
// Admin-Endpoints: kein SW-Timeout, direkt ans Netzwerk
|
|
if (url.pathname.startsWith('/api/admin/')) return;
|
|
|
|
// Mutationen (POST/PATCH/PUT/DELETE): mit Timeout, bei Offline → Queue
|
|
if (['POST', 'PATCH', 'PUT', 'DELETE'].includes(method)) {
|
|
event.respondWith((async () => {
|
|
try {
|
|
return await _fetchTimeout(event.request.clone(), 10000);
|
|
} catch {
|
|
if (!_isQueueable(url.pathname, method))
|
|
return new Response(JSON.stringify({ detail: 'Offline — keine Verbindung.' }),
|
|
{ status: 503, headers: { 'Content-Type': 'application/json' } });
|
|
if (_isMediaUpload(event.request))
|
|
return new Response(JSON.stringify({ detail: 'Medien-Upload offline nicht möglich.' }),
|
|
{ status: 503, headers: { 'Content-Type': 'application/json' } });
|
|
// In Queue speichern
|
|
let body = '';
|
|
try { body = await event.request.text(); } catch {}
|
|
await _queueAdd({
|
|
url: url.pathname + url.search,
|
|
method,
|
|
headers: { 'Content-Type': event.request.headers.get('Content-Type') || 'application/json' },
|
|
body,
|
|
createdAt: new Date().toISOString(),
|
|
});
|
|
try { await self.registration.sync.register('by-offline-mutations'); } catch {}
|
|
return new Response(JSON.stringify({ _queued: true, detail: 'Offline gespeichert — wird synchronisiert wenn Verbindung besteht.' }),
|
|
{ status: 202, headers: { 'Content-Type': 'application/json' } });
|
|
}
|
|
})());
|
|
return;
|
|
}
|
|
|
|
// Alle anderen API-Calls (GET ohne Caching): mit Timeout
|
|
event.respondWith(
|
|
_fetchTimeout(event.request, 8000).catch(() =>
|
|
new Response(JSON.stringify({ detail: 'Offline — keine Verbindung.' }),
|
|
{ status: 503, headers: { 'Content-Type': 'application/json' } })
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
// OSM-Kartenkacheln: eigener persistenter Cache (Cache-First mit LRU-Eviction)
|
|
if (url.hostname.endsWith('tile.openstreetmap.org')) {
|
|
event.respondWith(
|
|
caches.open(CACHE_TILES).then(cache =>
|
|
cache.match(event.request).then(cached => {
|
|
if (cached) return cached;
|
|
return fetch(event.request).then(response => {
|
|
if (response.ok) {
|
|
cache.put(event.request, response.clone())
|
|
.then(() => trimTileCache()) // im Hintergrund — blockiert respondWith nicht
|
|
.catch(() => {});
|
|
}
|
|
return response;
|
|
});
|
|
})
|
|
).catch(() => new Response('', { status: 503 }))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// CSS, Core-JS + Seiten-Module: Network-First mit ignoreSearch-Fallback für Offline
|
|
if (url.pathname.startsWith('/css/') || url.pathname.startsWith('/js/pages/')
|
|
|| url.pathname.startsWith('/js/app.js') || url.pathname.startsWith('/js/ui.js')
|
|
|| url.pathname.startsWith('/js/api.js') || url.pathname.startsWith('/js/worlds.js')) {
|
|
event.respondWith(
|
|
fetch(event.request)
|
|
.then(response => {
|
|
if (response.ok) {
|
|
const clone = response.clone();
|
|
caches.open(CACHE_STATIC).then(c => c.put(event.request, clone));
|
|
}
|
|
return response;
|
|
})
|
|
.catch(() => caches.match(event.request, { ignoreSearch: true })
|
|
.then(cached => cached || new Response('', { status: 503 })))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Navigation (index.html): immer Network-First
|
|
if (event.request.mode === 'navigate') {
|
|
event.respondWith(
|
|
fetch(event.request)
|
|
.then(response => {
|
|
const clone = response.clone();
|
|
caches.open(CACHE_STATIC).then(c => c.put(event.request, clone));
|
|
return response;
|
|
})
|
|
.catch(() => caches.match('/'))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Statische Assets: Cache-First
|
|
event.respondWith(
|
|
caches.match(event.request)
|
|
.then(cached => cached || fetch(event.request)
|
|
.then(response => {
|
|
if (response.ok && event.request.method === 'GET') {
|
|
const clone = response.clone();
|
|
caches.open(CACHE_STATIC).then(c => c.put(event.request, clone));
|
|
}
|
|
return response;
|
|
})
|
|
)
|
|
.catch(() => {
|
|
if (event.request.mode === 'navigate') return caches.match('/');
|
|
return new Response('', { status: 503 });
|
|
})
|
|
);
|
|
});
|
|
|
|
// ----------------------------------------------------------
|
|
// BACKGROUND SYNC — Queue abarbeiten wenn Verbindung wiederhergestellt
|
|
// ----------------------------------------------------------
|
|
self.addEventListener('sync', event => {
|
|
if (event.tag === 'by-offline-mutations') {
|
|
event.waitUntil(_processQueue());
|
|
}
|
|
});
|
|
|
|
// ----------------------------------------------------------
|
|
// MESSAGE — Tile-Vorausladung (Offline-Speicherung) + Queue-Steuerung
|
|
// ----------------------------------------------------------
|
|
self.addEventListener('message', event => {
|
|
if (event.data?.type === 'SKIP_WAITING') {
|
|
self.skipWaiting();
|
|
return;
|
|
}
|
|
if (event.data?.type === 'INVALIDATE_CACHE') {
|
|
// Cache-Timestamps für angegebene Pfade zurücksetzen → nächster Request geht ans Netz
|
|
const paths = event.data.paths || [];
|
|
paths.forEach(p => _cacheTs.delete(p));
|
|
return;
|
|
}
|
|
if (event.data?.type === 'PROCESS_QUEUE') {
|
|
event.waitUntil(_processQueue());
|
|
return;
|
|
}
|
|
if (event.data?.type === 'QUEUE_COUNT') {
|
|
_queueAll().then(items =>
|
|
event.source?.postMessage({ type: 'QUEUE_COUNT', count: items.length })
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (event.data?.type !== 'CACHE_TILES') return;
|
|
|
|
const urls = event.data.urls || [];
|
|
const source = event.source;
|
|
let done = 0;
|
|
const total = urls.length;
|
|
|
|
if (total === 0) {
|
|
source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done: 0, total: 0 });
|
|
return;
|
|
}
|
|
|
|
caches.open(CACHE_TILES).then(cache => {
|
|
const queue = [...urls];
|
|
|
|
function fetchBatch() {
|
|
if (queue.length === 0) {
|
|
source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done: total, total });
|
|
trimTileCache(); // nach Bulk-Vorausladen einmal trimmen
|
|
return;
|
|
}
|
|
const batch = queue.splice(0, 8);
|
|
Promise.all(batch.map(url =>
|
|
cache.match(url).then(cached => {
|
|
if (cached) { done++; return; }
|
|
return fetch(url, { mode: 'cors' })
|
|
.then(r => { if (r.ok) cache.put(url, r); done++; })
|
|
.catch(() => { done++; });
|
|
})
|
|
)).then(() => {
|
|
source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done, total });
|
|
setTimeout(fetchBatch, 30);
|
|
});
|
|
}
|
|
|
|
fetchBatch();
|
|
});
|
|
});
|
|
|
|
// ----------------------------------------------------------
|
|
// PUSH NOTIFICATIONS
|
|
// ----------------------------------------------------------
|
|
self.addEventListener('push', event => {
|
|
if (!event.data) return;
|
|
|
|
const data = event.data.json();
|
|
|
|
const options = {
|
|
body: data.body || '',
|
|
icon: data.icon || '/icons/icon-192.png',
|
|
badge: data.badge || '/icons/icon-192.png',
|
|
tag: data.tag || 'ban-yaro',
|
|
data: data.data || {},
|
|
actions: data.actions || [],
|
|
vibrate: data.vibrate || [100, 50, 100],
|
|
requireInteraction: data.requireInteraction || false,
|
|
};
|
|
|
|
// Chat-Nachricht
|
|
if (data.type === 'chat_message') {
|
|
options.tag = data.tag || `chat-${data.data?.conversation_id}`;
|
|
options.renotify = true;
|
|
options.actions = [
|
|
{ action: 'reply', title: 'Antworten' },
|
|
{ action: 'dismiss', title: 'Schließen' },
|
|
];
|
|
}
|
|
|
|
// Freundschaftsanfrage
|
|
if (data.type === 'friend_request') {
|
|
options.tag = 'friend-request';
|
|
options.actions = [{ action: 'view', title: 'Anzeigen' }];
|
|
}
|
|
|
|
// Giftköder-Alarm: besondere Darstellung
|
|
if (data.type === 'poison_alert') {
|
|
options.tag = 'poison-alert';
|
|
options.requireInteraction = true;
|
|
options.vibrate = [200, 100, 200, 100, 200];
|
|
options.actions = [
|
|
{ action: 'view', title: 'Auf Karte zeigen' },
|
|
{ action: 'dismiss', title: 'Verstanden' },
|
|
];
|
|
}
|
|
|
|
event.waitUntil(
|
|
self.registration.showNotification(data.title || 'Ban Yaro', options)
|
|
);
|
|
|
|
// App informieren damit sie Nearby-Alerts neu prüft
|
|
if (data.type === 'poison_alert' || data.type === 'lost_dog_alert') {
|
|
self.clients.matchAll({ type: 'window' }).then(clients => {
|
|
clients.forEach(c => c.postMessage({ type: 'CHECK_NEARBY_ALERTS' }));
|
|
});
|
|
}
|
|
});
|
|
|
|
// ----------------------------------------------------------
|
|
// NOTIFICATION CLICK
|
|
// ----------------------------------------------------------
|
|
self.addEventListener('notificationclick', event => {
|
|
event.notification.close();
|
|
|
|
const data = event.notification.data;
|
|
const action = event.action;
|
|
|
|
let url = '/';
|
|
|
|
if (action === 'view' || action === 'reply' || data?.page) {
|
|
url = `/#${data.page || 'poison'}`;
|
|
if (data.conversation_id) url += `?conversation_id=${data.conversation_id}`;
|
|
else if (data.id) url += `?id=${data.id}`;
|
|
}
|
|
|
|
event.waitUntil(
|
|
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
|
.then(windowClients => {
|
|
for (const client of windowClients) {
|
|
if (client.url.includes(self.location.origin)) {
|
|
client.focus();
|
|
client.navigate(url);
|
|
return;
|
|
}
|
|
}
|
|
return clients.openWindow(url);
|
|
})
|
|
);
|
|
});
|