Tagebuch:
- Day-One-Listenansicht: Wochentag + Tageszahl + Meta-Zeile (Zeit/Ort/Wetter)
- 4 Ansichten: Liste, Medien-Mosaik, Kalender (mit Sprungbuttons), Karte (GPS-Marker)
- Detail-Ansicht inline im Content-Bereich (kein Fullscreen-Overlay mehr)
- Hero-Bild vollständig sichtbar (object-fit:contain), Lightbox mit Safe-Area
- 2-Spalten-Layout Desktop: Text + Leaflet-Karte + POI-Liste
- EXIF-GPS-Extraktion bei Foto-Upload, historisches Wetter via Archive-API
- NoteStation-Import: Fotos in diary_media (80 Einträge migriert, 94 Medien)
- Stats-Endpoints: /diary/stats, /diary/calendar, /diary/locations
Notiz-Feature:
- Generische notes-Tabelle (parent_type + parent_id + meta_json)
- 📝-Button in 8 Bereichen, Notizblock-Seite mit KI-Analyse
- KI-Toggle in Einstellungen, notes_ki_enabled in User-Profil
Icons & Design:
- fill:currentColor Fix für welcome/onboarding/friends.js
- --c-icon Variable, --c-text-muted Dark Mode aufgehellt
- 15+ neue Phosphor-Icons aus lokaler Kopie
- CSS Network-First im SW, Cache-Control-Middleware
Infrastruktur:
- Wiki-Anreicherungs-Scheduler-Jobs entfernt (abgeschlossen)
- auth.py: notes_ki_enabled + is_social_media im User-Response
265 lines
8.1 KiB
JavaScript
265 lines
8.1 KiB
JavaScript
/* ============================================================
|
|
BAN YARO — Service Worker
|
|
Offline-Cache + Push Notifications + Tile-Cache
|
|
============================================================ */
|
|
|
|
const CACHE_VERSION = 'by-v405';
|
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
|
|
|
// index.html wird NICHT pre-gecacht (immer Network-First)
|
|
const STATIC_ASSETS = [
|
|
'/css/design-system.css?v=382',
|
|
'/css/layout.css?v=382',
|
|
'/css/components.css?v=382',
|
|
'/icons/phosphor.svg',
|
|
'/js/api.js',
|
|
'/js/ui.js',
|
|
'/js/app.js',
|
|
'/js/leaflet.markercluster.js',
|
|
'/css/MarkerCluster.css',
|
|
'/css/MarkerCluster.Default.css',
|
|
'/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 (CACHE_TILES behalten)
|
|
// ----------------------------------------------------------
|
|
self.addEventListener('activate', event => {
|
|
event.waitUntil(
|
|
caches.keys()
|
|
.then(keys => Promise.all(
|
|
keys
|
|
.filter(k => k !== CACHE_STATIC && k !== CACHE_TILES)
|
|
.map(k => caches.delete(k))
|
|
))
|
|
.then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
// ----------------------------------------------------------
|
|
// FETCH — Network-First für HTML, Cache-First für Assets, Network 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;
|
|
}
|
|
|
|
// OSM-Kartenkacheln: eigener persistenter Cache
|
|
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());
|
|
return response;
|
|
});
|
|
})
|
|
).catch(() => new Response('', { status: 503 }))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// CSS + Seiten-Module: immer Network-First — damit iOS nie veraltete CSS cached
|
|
if (url.pathname.startsWith('/css/') || url.pathname.startsWith('/js/pages/')) {
|
|
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))
|
|
);
|
|
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('/');
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
// ----------------------------------------------------------
|
|
// MESSAGE — Tile-Vorausladung (Offline-Speicherung)
|
|
// ----------------------------------------------------------
|
|
self.addEventListener('message', event => {
|
|
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 });
|
|
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);
|
|
})
|
|
);
|
|
});
|