UX: Offline-Pfote — automatischer Tile-Prefetch + Step 5 umgebaut, SW by-v1086

- Step 5 misst jetzt 'Welt-Daten' (Streak + Wetter) statt
  Wiki/Übungen — die kommen automatisch beim Welten-Aufruf, kein
  zusätzliches Prefetch nötig (User-Wunsch: Wiki+Übungen NICHT
  preloaden)
- Neuer _prefetchTiles(): beim App-Start werden 49+9 OSM-Tiles
  (Zoom 14 +13) im 3km-Umkreis automatisch gecacht — aber NUR wenn
  GPS-Permission schon erteilt ist (kein nerviger Popup beim
  Start). Damit wird Step 4 nach kurzer Zeit grün.
- _fetchMissing für Step 5 lädt jetzt Streak + Wetter (statt Wiki)
This commit is contained in:
rene 2026-05-26 15:25:54 +02:00
parent 94f02dbe3a
commit 307b4a5486
5 changed files with 65 additions and 18 deletions

View file

@ -101,9 +101,9 @@
</script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1085">
<link rel="stylesheet" href="/css/layout.css?v=1085">
<link rel="stylesheet" href="/css/components.css?v=1085">
<link rel="stylesheet" href="/css/design-system.css?v=1086">
<link rel="stylesheet" href="/css/layout.css?v=1086">
<link rel="stylesheet" href="/css/components.css?v=1086">
</head>
<body>
@ -625,11 +625,11 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1085"></script>
<script src="/js/ui.js?v=1085"></script>
<script src="/js/app.js?v=1085"></script>
<script src="/js/worlds.js?v=1085"></script>
<script src="/js/offline-indicator.js?v=1085"></script>
<script src="/js/api.js?v=1086"></script>
<script src="/js/ui.js?v=1086"></script>
<script src="/js/app.js?v=1086"></script>
<script src="/js/worlds.js?v=1086"></script>
<script src="/js/offline-indicator.js?v=1086"></script>
<!-- Feature-Seiten werden lazy geladen -->

View file

@ -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;

View file

@ -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);

View file

@ -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