UX: Offline-Pfote misst echte Offline-Bereitschaft, SW by-v1087

Steps so umverteilt dass sie genau die Datentypen abdecken die der
User offline braucht — auch wenn er die Seiten nie geöffnet hat:

  1 App-Grundgerüst     CSS + Core-JS
  2 Wichtige Seiten     alle 10 Page-Module (precached via SW)
  3 Hund-Daten          Profil + Tagebuch + Gesundheit
  4 Weitere Listen      Ausgaben + Routen + Notizen
  5 Karten-Kacheln      OSM-Tiles im Umkreis

Automatischer Prefetch im _prefetchData() beim App-Start:
- /api/expenses · /api/routes · /api/notes (Step 4)
- /api/dogs/{id}/health · /api/dogs/{id}/diary (Step 3)
- Tiles via _prefetchTiles wenn GPS-Permission da (Step 5)

Wiki, Übungen, Streak, Wetter werden NICHT mehr vorgeladen — kommen
beim normalen Welten-Besuch ins Cache, sind aber nicht Pflicht für
'offline-bereit'.

setTimeout-Retry nach 3s: aktiver Hund ist beim ersten Init oft
noch nicht in _appState, danach kommt der Health/Diary-Prefetch.
This commit is contained in:
rene 2026-05-26 15:34:42 +02:00
parent 307b4a5486
commit 87462cb2fe
5 changed files with 54 additions and 42 deletions

View file

@ -410,7 +410,7 @@ async def serve_media(path: str, request: _Request):
raise _HE(404, "Nicht gefunden.")
return _media_response(filepath)
APP_VER = "1086" # muss mit APP_VER in app.js übereinstimmen
APP_VER = "1087" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json")
async def assetlinks():

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=1086">
<link rel="stylesheet" href="/css/layout.css?v=1086">
<link rel="stylesheet" href="/css/components.css?v=1086">
<link rel="stylesheet" href="/css/design-system.css?v=1087">
<link rel="stylesheet" href="/css/layout.css?v=1087">
<link rel="stylesheet" href="/css/components.css?v=1087">
</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=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>
<script src="/js/api.js?v=1087"></script>
<script src="/js/ui.js?v=1087"></script>
<script src="/js/app.js?v=1087"></script>
<script src="/js/worlds.js?v=1087"></script>
<script src="/js/offline-indicator.js?v=1087"></script>
<!-- Feature-Seiten werden lazy geladen -->

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1086'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1087'; // ← 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

@ -40,33 +40,35 @@ window.OfflineIndicator = (() => {
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',
{ step: 3, title: 'Hund-Daten',
detail: 'Profil, Tagebuch und Gesundheit',
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));
return urls.some(u => /\/api\/dogs\/\d+(\?|$)/.test(u))
&& urls.some(u => /\/api\/dogs\/\d+\/diary/.test(u))
&& urls.some(u => /\/api\/dogs\/\d+\/health/.test(u));
} },
{ step: 4, title: 'Karten-Kacheln',
detail: `Mindestens ${TILE_MIN} Tiles im Umkreis`,
{ step: 4, title: 'Weitere Listen',
detail: 'Ausgaben, Routen, Notizen',
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/expenses'))
&& urls.some(u => u.includes('/api/routes'))
&& urls.some(u => u.includes('/api/notes'));
} },
{ step: 5, title: 'Karten-Kacheln',
detail: `Mindestens ${TILE_MIN} OSM-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: '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/streak/'))
&& urls.some(u => u.includes('/api/weather'));
} },
];
// Tile-Prefetch-Konfiguration
@ -154,23 +156,14 @@ window.OfflineIndicator = (() => {
if (dogId) {
tasks.push(fetch(`/api/dogs/${dogId}`).catch(() => {}));
tasks.push(fetch(`/api/dogs/${dogId}/diary?limit=20`).catch(() => {}));
tasks.push(fetch(`/api/dogs/${dogId}/health`).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,
});
}
}
tasks.push(fetch('/api/expenses').catch(() => {}));
tasks.push(fetch('/api/routes').catch(() => {}));
tasks.push(fetch('/api/notes').catch(() => {}));
} else if (m.step === 5) {
// 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 _prefetchTiles();
}
}
await Promise.all(tasks);
@ -214,9 +207,28 @@ window.OfflineIndicator = (() => {
} catch {}
}
// Daten-Prefetch beim App-Start: Listen die offline brauchbar sein müssen,
// auch wenn der User die Seiten noch nie geöffnet hat
async function _prefetchData() {
fetch('/api/expenses').catch(() => {});
fetch('/api/routes').catch(() => {});
fetch('/api/notes').catch(() => {});
// Hund-spezifische Daten nur wenn aktiver Hund bekannt
const dogId = window._appState?.activeDog?.id;
if (dogId) {
fetch(`/api/dogs/${dogId}/health`).catch(() => {});
fetch(`/api/dogs/${dogId}/diary?limit=20`).catch(() => {});
}
}
function init() {
refresh();
_prefetchTiles(); // im Hintergrund
_prefetchTiles(); // Karten-Tiles (nur wenn GPS schon erlaubt)
_prefetchData(); // Listen-Daten (Expenses, Routes, Notes, Health)
// Wenn der aktive Hund erst nach init() gesetzt wird → nochmal triggern
setTimeout(() => { _prefetchData(); refresh(); }, 3000);
if (navigator.serviceWorker) {
navigator.serviceWorker.addEventListener('message', e => {
if (e?.data?.type === 'CACHE_UPDATE') refresh();

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1086';
const VER = '1087';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten