diff --git a/backend/main.py b/backend/main.py index bf3a3bc..814445c 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 = "1082" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "1083" # 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 ee134f5..a26655f 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -8867,44 +8867,17 @@ svg.empty-state-icon { } /* ============================================================ - Offline-Bereitschafts-Indikator — schwebend über dem Welten-FAB - Sichtbar NUR wenn Welten aktiv sind (Sibling-Selektor) - 5 Pfade — Score 0 (grau) bis 5 (grün, gefüllt) + Offline-Bereitschafts-Anzeige IM Welten-FAB + Die 5 Pfoten-Pfade werden je nach Score grün gefärbt + (Default = weiß auf orange, filled = grün auf orange) ============================================================ */ -#offline-indicator { - display: flex; /* Default: sichtbar — JS blendet auf Detail-Seiten aus */ - position: fixed; - right: 20px; /* gleicher right wie #worlds-fab */ - bottom: calc(env(safe-area-inset-bottom, 16px) + 16px + 54px + 12px); /* FAB-Bottom + FAB-Höhe + 12px */ - width: 40px; - height: 40px; - border-radius: 50%; - background: rgba(255,255,255,0.95); - border: 2px solid var(--c-border); - box-shadow: 0 2px 10px rgba(0,0,0,0.18); - align-items: center; - justify-content: center; - padding: 0; - cursor: pointer; - z-index: 61; /* knapp über dem FAB (60), unter Modals */ - transition: transform 0.12s, opacity 0.2s; +#worlds-fab .offline-paw .paw-elem { + color: #fff; + transition: stroke 0.4s ease, fill 0.4s ease; } -#offline-indicator.is-hidden { display: none; } /* JS-gesteuert: in Detail-Seiten */ - -[data-theme="dark"] #offline-indicator { - background: rgba(31,41,55,0.85); - border-color: rgba(255,255,255,0.08); -} -#offline-indicator:active { transform: scale(0.92); } -#offline-indicator .offline-paw { width: 24px; height: 24px; } - -.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); +#worlds-fab .offline-paw .paw-elem.filled { + color: #16a34a; /* leuchtendes Grün, klar sichtbar auf orange */ + fill: #16a34a; } .offline-status-row { diff --git a/backend/static/index.html b/backend/static/index.html index b8426aa..236ea78 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -101,9 +101,9 @@ - - - + + + @@ -602,27 +602,22 @@
- - - @@ -630,11 +625,11 @@ - - - - - + + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index f882e5a..940be82 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 = '1082'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1083'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen. diff --git a/backend/static/js/offline-indicator.js b/backend/static/js/offline-indicator.js index ba2971d..5f8336b 100644 --- a/backend/static/js/offline-indicator.js +++ b/backend/static/js/offline-indicator.js @@ -1,7 +1,8 @@ /* ============================================================ - BAN YARO — Offline-Bereitschafts-Indikator - 5-stufige Pfote im Header, zeigt wie viel der App offline - verfügbar ist. Klick → Status-Modal mit Nachlade-Button. + BAN YARO — Offline-Bereitschafts-Anzeige IM Welten-FAB + Färbt die 5 Pfoten-Pfade je nach Cache-Stand grün: + 1 = App-Shell · 2 = Wichtige Seiten · 3 = Hund-/Tagebuchdaten + 4 = Karten-Tiles · 5 = Training & Wissen ============================================================ */ window.OfflineIndicator = (() => { @@ -11,9 +12,8 @@ window.OfflineIndicator = (() => { const CACHE_STATIC = `by-v${(window.APP_VER || '0')}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; const CACHE_API = 'ban-yaro-api-v1'; - const TILE_MIN = 50; // Mindest-Tiles für Stufe 4 + const TILE_MIN = 50; - // 5 Offline-Bereitschafts-Checks, in Reihenfolge der Pfoten-Stufen const CHECKS = [ { step: 1, title: 'App-Grundgerüst', detail: 'CSS, Layout und Hauptmodule — die Basis', @@ -27,8 +27,8 @@ window.OfflineIndicator = (() => { if (!c) return false; const must = ['diary.js','map.js','walks.js','erste-hilfe.js']; const keys = await c.keys(); - const have = new Set(keys.map(r => r.url)); - return must.every(name => [...have].some(u => u.includes('/js/pages/' + name))); + const have = keys.map(r => r.url); + return must.every(name => have.some(u => u.includes('/js/pages/' + name))); } }, { step: 3, title: 'Hund- und Tagebuchdaten', @@ -36,8 +36,7 @@ window.OfflineIndicator = (() => { probe: async () => { const c = await caches.open(CACHE_API).catch(() => null); if (!c) return false; - const keys = await c.keys(); - const urls = keys.map(r => r.url); + const urls = (await c.keys()).map(r => r.url); return urls.some(u => /\/api\/dogs\/\d+/.test(u)) && urls.some(u => /\/api\/dogs\/\d+\/diary/.test(u)); } }, @@ -47,12 +46,11 @@ window.OfflineIndicator = (() => { probe: async () => { const c = await caches.open(CACHE_TILES).catch(() => null); if (!c) return false; - const keys = await c.keys(); - return keys.length >= TILE_MIN; + return (await c.keys()).length >= TILE_MIN; } }, { step: 5, title: 'Training & Wissen', - detail: 'Übungen, Wiki-Rassen, Wetter — Welt-Inhalte', + detail: 'Übungen, Wiki-Rassen, Wetter', probe: async () => { const c = await caches.open(CACHE_API).catch(() => null); if (!c) return false; @@ -62,47 +60,34 @@ window.OfflineIndicator = (() => { } }, ]; - let _btn = null; - let _svg = null; - let _lastScore = -1; + let _fab = null; - // ---------------------------------------------------------- - // Score berechnen + Pfote einfärben - // ---------------------------------------------------------- async function refresh() { - if (!_btn) return; - if (!('caches' in window)) { _btn.style.display = 'none'; return; } + _fab = document.getElementById('worlds-fab'); + if (!_fab || !('caches' in window)) return null; const results = await Promise.all(CHECKS.map(async c => { try { return { ...c, ok: await c.probe() }; } catch { return { ...c, ok: false }; } })); - const score = results.filter(r => r.ok).length; - _applyScore(score, results); - _lastScore = score; - return { score, results }; - } - - function _applyScore(score, results) { - if (!_svg) return; - _svg.querySelectorAll('.paw-elem').forEach(el => { + _fab.querySelectorAll('.paw-elem').forEach(el => { const step = Number(el.dataset.step); const isOk = results.find(r => r.step === step)?.ok; el.classList.toggle('filled', !!isOk); }); - _btn.title = `Offline-Bereitschaft: ${score} von 5`; - _btn.setAttribute('aria-label', `Offline-Bereitschaft: ${score} von 5`); + + const score = results.filter(r => r.ok).length; + _fab.setAttribute('data-offline-score', `${score}/5`); + return { score, results }; } - // ---------------------------------------------------------- - // Status-Modal beim Klick - // ---------------------------------------------------------- - async function _openModal() { + // Optional aufrufbar: zeigt das Status-Modal mit Nachlade-Button + async function openStatus() { const data = await refresh(); if (!data) return; const { score, results } = data; - + const missing = results.filter(r => !r.ok); const rows = results.map(r => `
${r.ok ? '✓' : '○'}
@@ -113,25 +98,21 @@ window.OfflineIndicator = (() => {
`).join(''); - const missing = results.filter(r => !r.ok); - const allOk = missing.length === 0; - UI.modal.open({ title: `🐾 Offline-Bereitschaft ${score}/5`, body: `

- ${allOk - ? 'Deine App ist voll offline-fähig. Du kannst Tagebuch, Karte und Daten auch ohne Internet nutzen.' - : 'Je grüner deine Pfote, desto besser klappt die App ohne Internet. Fehlende Inhalte werden beim nächsten Online-Aufruf automatisch geladen.'} + ${missing.length === 0 + ? 'Voll offline-fähig — Tagebuch, Karte und Daten funktionieren auch ohne Internet.' + : 'Je grüner deine Pfote im FAB, desto mehr klappt offline. Fehlende Inhalte werden beim nächsten Online-Aufruf automatisch geladen.'}

${rows} `, footer: `
- ${missing.length - ? `` : ''} + ${missing.length ? `` : ''}
`, @@ -139,8 +120,7 @@ window.OfflineIndicator = (() => { document.getElementById('offline-fill-btn')?.addEventListener('click', async () => { const btn = document.getElementById('offline-fill-btn'); - btn.disabled = true; - btn.textContent = 'Lade …'; + btn.disabled = true; btn.textContent = 'Lade …'; await _fetchMissing(missing); UI.modal.close(); UI.toast.success('Offline-Inhalte aktualisiert.'); @@ -148,14 +128,10 @@ window.OfflineIndicator = (() => { }); } - // ---------------------------------------------------------- - // 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) { @@ -165,17 +141,13 @@ window.OfflineIndicator = (() => { 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, + type: 'CACHE_TILES', lat: pos.coords.latitude, lon: pos.coords.longitude, + zoom: 14, radius: 2, }); } } @@ -188,39 +160,8 @@ window.OfflineIndicator = (() => { await Promise.all(tasks); } - // ---------------------------------------------------------- - // Sichtbarkeit an Welten-Overlay koppeln - // — default sichtbar; nur ausblenden wenn explizit auf Detail-Seite - // ---------------------------------------------------------- - function _syncVisibility() { - if (!_btn) return; - const ov = document.getElementById('worlds-overlay'); - if (!ov) return; // ohne Welten-Overlay sichtbar lassen - const inWorlds = ov.classList.contains('worlds-visible') - || ov.style.display === 'block' - || ov.style.display === ''; - _btn.classList.toggle('is-hidden', !inWorlds); - } - - // ---------------------------------------------------------- - // Init - // ---------------------------------------------------------- function init() { - _btn = document.getElementById('offline-indicator'); - if (!_btn) { console.warn('[OfflineIndicator] #offline-indicator nicht im DOM'); return; } - _svg = _btn.querySelector('.offline-paw'); - _btn.addEventListener('click', _openModal); - - // MutationObserver: Welten-Overlay Klassenänderung → Indikator zeigen/verstecken - const ov = document.getElementById('worlds-overlay'); - if (ov) { - _syncVisibility(); - new MutationObserver(_syncVisibility).observe(ov, { attributes: true, attributeFilter: ['class', 'style'] }); - } - 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(); @@ -229,7 +170,7 @@ window.OfflineIndicator = (() => { setInterval(refresh, 60_000); } - return { init, refresh }; + return { init, refresh, openStatus }; })(); if (document.readyState !== 'loading') { diff --git a/backend/static/sw.js b/backend/static/sw.js index c6b47d2..8d364e3 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 = '1082'; +const VER = '1083'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten