diff --git a/backend/main.py b/backend/main.py index 376e2c1..928c345 100644 --- a/backend/main.py +++ b/backend/main.py @@ -410,7 +410,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "1085" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "1086" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/index.html b/backend/static/index.html index 30114e6..fed4422 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -101,9 +101,9 @@ - - - + + + @@ -625,11 +625,11 @@ - - - - - + + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 1375f40..ff6bcce 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '1085'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1086'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator) window.APP_VERSION = APP_VERSION; diff --git a/backend/static/js/offline-indicator.js b/backend/static/js/offline-indicator.js index 48f2566..a895b24 100644 --- a/backend/static/js/offline-indicator.js +++ b/backend/static/js/offline-indicator.js @@ -58,17 +58,23 @@ window.OfflineIndicator = (() => { return (await c.keys()).length >= TILE_MIN; } }, - { step: 5, title: 'Training & Wissen', - detail: 'Übungen, Wiki-Rassen, Wetter', + { step: 5, title: 'Welt-Daten', + detail: 'Streak, Wetter, Hundepass — kommt beim Welten-Aufruf', probe: async () => { const c = await caches.open(CACHE_API).catch(() => null); if (!c) return false; const urls = (await c.keys()).map(r => r.url); - return urls.some(u => u.includes('/api/training/exercises')) - && urls.some(u => u.includes('/api/wiki/rassen')); + return urls.some(u => u.includes('/api/streak/')) + && urls.some(u => u.includes('/api/weather')); } }, ]; + // Tile-Prefetch-Konfiguration + const TILE_PREFETCH = [ + { zoom: 14, radius: 3 }, // 7x7 = 49 Tiles im Nahbereich + { zoom: 13, radius: 1 }, // 3x3 = 9 Tiles für Übersicht + ]; + let _fab = null; async function refresh() { @@ -161,19 +167,60 @@ window.OfflineIndicator = (() => { } } } else if (m.step === 5) { - tasks.push(fetch('/api/training/exercises').catch(() => {})); - tasks.push(fetch('/api/wiki/rassen?limit=50').catch(() => {})); + // Welt-Daten: Streak braucht Hund-ID, Wetter braucht GPS tasks.push(fetch('/api/weather').catch(() => {})); + const dogId = window._appState?.activeDog?.id; + if (dogId) tasks.push(fetch(`/api/streak/${dogId}`).catch(() => {})); } } await Promise.all(tasks); } + // ---------------------------------------------------------- + // Tile-URL-Berechnung (OSM, Subdomain 'a') + // ---------------------------------------------------------- + function _tile(lat, lon, z) { + const n = Math.pow(2, z); + const x = Math.floor((lon + 180) / 360 * n); + const latRad = lat * Math.PI / 180; + const y = Math.floor((1 - Math.log(Math.tan(latRad) + 1/Math.cos(latRad)) / Math.PI) / 2 * n); + return { x, y }; + } + function _tileUrls(lat, lon, zoom, radius) { + const center = _tile(lat, lon, zoom); + const out = []; + for (let dx = -radius; dx <= radius; dx++) { + for (let dy = -radius; dy <= radius; dy++) { + out.push(`https://a.tile.openstreetmap.org/${zoom}/${center.x + dx}/${center.y + dy}.png`); + } + } + return out; + } + + // Tile-Prefetch im Umkreis der aktuellen GPS-Position (nur wenn Permission schon da) + async function _prefetchTiles() { + if (!navigator.serviceWorker?.controller) return; + if (!navigator.permissions || !navigator.geolocation) return; + try { + const perm = await navigator.permissions.query({ name: 'geolocation' }); + if (perm.state !== 'granted') return; // kein Popup wenn nicht schon erlaubt + const pos = await new Promise(res => + navigator.geolocation.getCurrentPosition(p => res(p), () => res(null), { timeout: 5000 })); + if (!pos) return; + const urls = []; + TILE_PREFETCH.forEach(({ zoom, radius }) => + urls.push(..._tileUrls(pos.coords.latitude, pos.coords.longitude, zoom, radius))); + navigator.serviceWorker.controller.postMessage({ type: 'CACHE_TILES', urls }); + } catch {} + } + function init() { refresh(); + _prefetchTiles(); // im Hintergrund if (navigator.serviceWorker) { navigator.serviceWorker.addEventListener('message', e => { - if (e?.data?.type === 'CACHE_UPDATE') refresh(); + if (e?.data?.type === 'CACHE_UPDATE') refresh(); + if (e?.data?.type === 'CACHE_TILES_PROGRESS') refresh(); }); } setInterval(refresh, 60_000); diff --git a/backend/static/sw.js b/backend/static/sw.js index 19811d7..59c9a12 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -4,7 +4,7 @@ ============================================================ */ // ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab -const VER = '1085'; +const VER = '1086'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten