/* ============================================================ 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 = (() => { 'use strict'; // Cache-Namen dynamisch finden — robust gegen Versions-Updates const CACHE_TILES = 'ban-yaro-tiles-v1'; const CACHE_API = 'ban-yaro-api-v1'; const TILE_MIN = 50; async function _staticCache() { const names = await caches.keys(); const found = names.find(n => /^by-v\d+-static$/.test(n)); return found ? await caches.open(found) : null; } const CHECKS = [ { step: 1, title: 'App-Grundgerüst', detail: 'CSS, Layout und Hauptmodule — die Basis', probe: async () => { const c = await _staticCache(); if (!c) return false; const urls = (await c.keys()).map(r => r.url); return urls.some(u => u.includes('/css/design-system.css')) && urls.some(u => u.includes('/js/app.js')); } }, { step: 2, title: 'Wichtige Seiten', detail: 'Tagebuch, Karte, Gassi, Erste Hilfe, Notizblock, Ausgaben, Routen', probe: async () => { const c = await _staticCache(); if (!c) return false; const must = ['diary.js','map.js','walks.js','erste-hilfe.js','notes.js','expenses.js','routes.js']; const urls = (await c.keys()).map(r => r.url); return must.every(name => urls.some(u => u.includes('/js/pages/' + name))); } }, { step: 3, title: 'Hund- und Tagebuchdaten', detail: 'Letzte Einträge und Hund-Profil', 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 => /\/api\/dogs\/\d+/.test(u)) && urls.some(u => /\/api\/dogs\/\d+\/diary/.test(u)); } }, { step: 4, title: 'Karten-Kacheln', detail: `Mindestens ${TILE_MIN} Tiles im Umkreis`, probe: async () => { const c = await caches.open(CACHE_TILES).catch(() => null); if (!c) return false; return (await c.keys()).length >= TILE_MIN; } }, { step: 5, title: 'Training & Wissen', detail: 'Übungen, Wiki-Rassen, Wetter', 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')); } }, ]; let _fab = null; async function refresh() { _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 }; } })); _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); }); const score = results.filter(r => r.ok).length; _fab.setAttribute('data-offline-score', `${score}/5`); return { score, results }; } // 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 ? '✓' : '○'}
${r.title}
${r.detail}
`).join(''); UI.modal.open({ title: `🐾 Offline-Bereitschaft ${score}/5`, body: `

${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 ? `` : ''}
`, }); 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(); }); } async function _fetchMissing(missing) { const tasks = []; for (const m of missing) { if (m.step === 2) { ['diary.js','map.js','walks.js','erste-hilfe.js','notes.js','expenses.js','routes.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) { 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) { tasks.push(fetch('/api/training/exercises').catch(() => {})); tasks.push(fetch('/api/wiki/rassen?limit=50').catch(() => {})); tasks.push(fetch('/api/weather').catch(() => {})); } } await Promise.all(tasks); } function init() { refresh(); if (navigator.serviceWorker) { navigator.serviceWorker.addEventListener('message', e => { if (e?.data?.type === 'CACHE_UPDATE') refresh(); }); } setInterval(refresh, 60_000); } return { init, refresh, openStatus }; })(); if (document.readyState !== 'loading') { window.OfflineIndicator.init(); } else { document.addEventListener('DOMContentLoaded', () => window.OfflineIndicator.init()); }