Sprint 0: Design System, App Shell, PWA, zentrales JS-Fundament
This commit is contained in:
parent
756e17faba
commit
84f49fafcf
9 changed files with 2507 additions and 0 deletions
151
backend/static/sw.js
Normal file
151
backend/static/sw.js
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/* ============================================================
|
||||
BAN YARO — Service Worker
|
||||
Offline-Cache + Push Notifications
|
||||
============================================================ */
|
||||
|
||||
const CACHE_VERSION = 'by-v1';
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
|
||||
// Diese Dateien werden beim Install gecacht (App Shell)
|
||||
const STATIC_ASSETS = [
|
||||
'/',
|
||||
'/css/design-system.css',
|
||||
'/css/layout.css',
|
||||
'/css/components.css',
|
||||
'/js/api.js',
|
||||
'/js/ui.js',
|
||||
'/js/app.js',
|
||||
'/manifest.json',
|
||||
'/icons/icon-192.png',
|
||||
];
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// INSTALL — App Shell cachen
|
||||
// ----------------------------------------------------------
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_STATIC)
|
||||
.then(cache => cache.addAll(STATIC_ASSETS))
|
||||
.then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// ACTIVATE — alte Caches aufräumen
|
||||
// ----------------------------------------------------------
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(keys => Promise.all(
|
||||
keys.filter(k => k !== CACHE_STATIC).map(k => caches.delete(k))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// FETCH — Cache-First für statische Assets, Network-First für API
|
||||
// ----------------------------------------------------------
|
||||
self.addEventListener('fetch', event => {
|
||||
const url = new URL(event.request.url);
|
||||
|
||||
// API-Calls: immer Network, kein Cache
|
||||
if (url.pathname.startsWith('/api/')) {
|
||||
event.respondWith(
|
||||
fetch(event.request).catch(() =>
|
||||
new Response(JSON.stringify({ detail: 'Offline — keine Verbindung.' }),
|
||||
{ status: 503, headers: { 'Content-Type': 'application/json' } })
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Statische Assets: Cache-First
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then(cached => cached || fetch(event.request)
|
||||
.then(response => {
|
||||
// Erfolgreiche Responses für statische Assets cachen
|
||||
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(() => {
|
||||
// Offline-Fallback: App Shell zurückgeben
|
||||
if (event.request.mode === 'navigate') {
|
||||
return caches.match('/');
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// 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,
|
||||
};
|
||||
|
||||
// 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)
|
||||
);
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// NOTIFICATION CLICK
|
||||
// ----------------------------------------------------------
|
||||
self.addEventListener('notificationclick', event => {
|
||||
event.notification.close();
|
||||
|
||||
const data = event.notification.data;
|
||||
const action = event.action;
|
||||
|
||||
let url = '/';
|
||||
|
||||
if (action === 'view' || data?.page) {
|
||||
url = `/#${data.page || 'poison'}`;
|
||||
if (data.id) url += `?id=${data.id}`;
|
||||
}
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window', includeUncontrolled: true })
|
||||
.then(windowClients => {
|
||||
// Offenes Fenster fokussieren
|
||||
for (const client of windowClients) {
|
||||
if (client.url.includes(self.location.origin)) {
|
||||
client.focus();
|
||||
client.navigate(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Neues Fenster öffnen
|
||||
return clients.openWindow(url);
|
||||
})
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue