diff --git a/backend/main.py b/backend/main.py index f22fee0..d4f52c9 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 = "1076" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "1077" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 41d4e27..24186fe 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -8865,3 +8865,51 @@ svg.empty-state-icon { overflow: hidden; position: relative; } + +/* ============================================================ + Offline-Bereitschafts-Indikator (Pfote im Header) + 5 Pfade — Score 0 (grau) bis 5 (grün, gefüllt) + ============================================================ */ +.offline-paw .paw-elem { + color: var(--c-text-muted); + transition: stroke 0.5s ease, fill 0.5s ease; +} +.offline-paw .paw-elem.filled { + color: var(--c-success); + fill: var(--c-success); +} +#offline-indicator { + background: none; + border: none; + cursor: pointer; +} +#offline-indicator:hover .paw-elem { + opacity: 0.85; +} + +.offline-status-row { + display: flex; + align-items: center; + gap: var(--space-3); + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-md); + border: 1px solid var(--c-border-light); + font-size: var(--text-sm); + margin-bottom: var(--space-2); +} +.offline-status-row .osr-check { + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 14px; + font-weight: 700; +} +.offline-status-row.ok .osr-check { background: var(--c-success); color: #fff; } +.offline-status-row.miss .osr-check { background: var(--c-surface-2); color: var(--c-text-muted); border: 1px dashed var(--c-border); } +.offline-status-row .osr-text { flex: 1; min-width: 0; } +.offline-status-row .osr-title { font-weight: 600; } +.offline-status-row .osr-detail { font-size: var(--text-xs); color: var(--c-text-muted); margin-top: 2px; } diff --git a/backend/static/index.html b/backend/static/index.html index 8cef0ca..a85acae 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -101,9 +101,9 @@ - - - + + + @@ -325,6 +325,25 @@ Ban Yaro
+ ` : ''} + + + `, + }); + + document.getElementById('offline-fill-btn')?.addEventListener('click', async () => { + const btn = document.getElementById('offline-fill-btn'); + btn.disabled = true; + btn.textContent = 'Lade …'; + await _fetchMissing(missing); + UI.modal.close(); + UI.toast.success('Offline-Inhalte aktualisiert.'); + refresh(); + }); + } + + // ---------------------------------------------------------- + // Fehlende Inhalte aktiv nachladen + // ---------------------------------------------------------- + async function _fetchMissing(missing) { + const tasks = []; + for (const m of missing) { + if (m.step === 2) { + // Page-Module fetchen → SW cached sie automatisch + ['diary.js','map.js','walks.js','erste-hilfe.js'].forEach(p => + tasks.push(fetch(`/js/pages/${p}?v=${window.APP_VER}`).catch(() => {}))); + } else if (m.step === 3) { + const dogId = window._appState?.activeDog?.id; + if (dogId) { + tasks.push(fetch(`/api/dogs/${dogId}`).catch(() => {})); + tasks.push(fetch(`/api/dogs/${dogId}/diary?limit=20`).catch(() => {})); + } + } else if (m.step === 4) { + // Karten-Tiles: SW per Message anstoßen + if (navigator.serviceWorker?.controller) { + const pos = await new Promise(res => + navigator.geolocation?.getCurrentPosition(p => res(p), () => res(null), { timeout: 4000 })); + if (pos) { + navigator.serviceWorker.controller.postMessage({ + type: 'CACHE_TILES', + lat: pos.coords.latitude, + lon: pos.coords.longitude, + zoom: 14, + radius: 2, + }); + } + } + } else if (m.step === 5) { + const dogId = window._appState?.activeDog?.id; + if (dogId) { + try { + const entries = await fetch(`/api/dogs/${dogId}/diary?limit=10`).then(r => r.json()); + (entries || []).slice(0, 10).forEach(e => { + if (e.cover_url) tasks.push(fetch(e.cover_url).catch(() => {})); + (e.media_items || []).slice(0, 3).forEach(m => { + if (m.url) tasks.push(fetch(m.url).catch(() => {})); + }); + }); + } catch {} + } + } + } + await Promise.all(tasks); + } + + // ---------------------------------------------------------- + // Init + // ---------------------------------------------------------- + function init() { + _btn = document.getElementById('offline-indicator'); + if (!_btn) return; + _svg = _btn.querySelector('.offline-paw'); + _btn.addEventListener('click', _openModal); + refresh(); + + // bei SW-Updates und alle 60s neu prüfen + if (navigator.serviceWorker) { + navigator.serviceWorker.addEventListener('message', e => { + if (e?.data?.type === 'CACHE_UPDATE') refresh(); + }); + } + setInterval(refresh, 60_000); + } + + return { init, refresh }; +})(); + +if (document.readyState !== 'loading') { + window.OfflineIndicator.init(); +} else { + document.addEventListener('DOMContentLoaded', () => window.OfflineIndicator.init()); +} diff --git a/backend/static/sw.js b/backend/static/sw.js index 114fc2d..9e0ba5a 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 = '1076'; +const VER = '1077'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten @@ -32,6 +32,7 @@ const STATIC_ASSETS = [ `/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',