diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js index f04bf67..c708912 100644 --- a/backend/static/js/pages/diary.js +++ b/backend/static/js/pages/diary.js @@ -6,6 +6,8 @@ window.Page_diary = (() => { + const _CACHE_KEY = 'by_diary_cache'; + // ---------------------------------------------------------- // MODUL-STATE // ---------------------------------------------------------- @@ -324,6 +326,7 @@ window.Page_diary = (() => { async function _load() { const dog = _appState.activeDog; if (!dog) return; + const cacheKey = _CACHE_KEY + '_' + dog.id; try { const params = { limit: LIMIT, offset: _offset }; if (_searchQuery) params.q = _searchQuery; @@ -331,6 +334,10 @@ window.Page_diary = (() => { const batch = await API.diary.list(dog.id, params); _entries = _entries.concat(batch); + if (_offset === 0 && !_searchQuery && !_filterMilestone) { + try { localStorage.setItem(cacheKey, JSON.stringify({ ts: Date.now(), data: batch })); } catch {} + } + // "Mehr laden" anzeigen wenn volle Page geladen wurde const loadMore = _container.querySelector('#diary-load-more'); if (loadMore) { @@ -339,7 +346,17 @@ window.Page_diary = (() => { // Stats-Bar befüllen _renderStatsBar(); - } catch (err) { + } catch { + try { + const raw = localStorage.getItem(cacheKey); + if (raw) { + const cached = JSON.parse(raw).data || []; + _entries = cached; + _renderStatsBar(); + UI.toast.info('Offline — zeige zuletzt geladene Einträge.'); + return; + } + } catch {} UI.toast.error('Einträge konnten nicht geladen werden.'); } } diff --git a/backend/static/js/pages/lost.js b/backend/static/js/pages/lost.js index 086224e..37daa9f 100644 --- a/backend/static/js/pages/lost.js +++ b/backend/static/js/pages/lost.js @@ -5,6 +5,43 @@ window.Page_lost = (() => { + // ---------------------------------------------------------- + // OFFLINE-CACHE + // ---------------------------------------------------------- + const _CACHE_KEY = 'by_lost_cache'; + const _PENDING_KEY = 'by_lost_pending'; + + function _getPending() { + try { return JSON.parse(localStorage.getItem(_PENDING_KEY) || '[]'); } catch { return []; } + } + function _setPending(list) { + try { localStorage.setItem(_PENDING_KEY, JSON.stringify(list)); } catch {} + } + function _addPending(data) { + const list = _getPending(); + const entry = { ...data, id: `pending_${Date.now()}`, _isPending: true, + created_at: new Date().toISOString() }; + list.push(entry); + _setPending(list); + return entry; + } + async function _syncPending() { + if (!navigator.onLine) return; + const list = _getPending(); + if (!list.length) return; + let ok = 0; + for (const item of [...list]) { + try { + const { id: _pid, _isPending, ...payload } = item; + await API.lost.report(payload); + _setPending(_getPending().filter(x => x.id !== item.id)); + ok++; + } catch {} + } + if (ok > 0) { UI.toast.success(`${ok} Meldung(en) synchronisiert.`); _loadReports(); } + } + window.addEventListener('online', _syncPending); + // ---------------------------------------------------------- // MODUL-STATE // ---------------------------------------------------------- @@ -179,8 +216,14 @@ window.Page_lost = (() => { return; } + const pending = _getPending().map(p => ({ + ...p, + distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon), + })); try { - _reports = await API.lost.list(_userPos.lat, _userPos.lon, 25); + const fetched = await API.lost.list(_userPos.lat, _userPos.lon, 25); + try { localStorage.setItem(_CACHE_KEY, JSON.stringify({ ts: Date.now(), data: fetched })); } catch {} + _reports = [...pending, ...fetched]; _renderMarkers(); _renderHeld(); _renderList(); @@ -191,6 +234,26 @@ window.Page_lost = (() => { : 'Keine vermissten Hunde in deiner Nähe (25 km Radius). 🐾'; } } catch { + try { + const raw = localStorage.getItem(_CACHE_KEY); + if (raw) { + _reports = [...pending, ...(JSON.parse(raw).data || [])]; + _renderMarkers(); + _renderHeld(); + _renderList(); + _updateBadge(_reports.length); + if (infoEl) infoEl.textContent = 'Offline — zeige zuletzt geladene Meldungen.'; + return; + } + } catch {} + _reports = pending; + if (pending.length) { + _renderMarkers(); + _renderHeld(); + _renderList(); + _updateBadge(_reports.length); + return; + } UI.toast.error('Meldungen konnten nicht geladen werden.'); } } @@ -332,6 +395,7 @@ window.Page_lost = (() => { Gemeldet ${_fmtDate(r.created_at)} ${r.melder_name ? '· ' + _escape(r.melder_name.split(' ')[0]) : ''} + ${r._isPending ? `
⏳ Sync ausstehend
` : ''} ${_appState.user ? `
@@ -1128,15 +1182,24 @@ window.Page_walks = (() => { const idx = _data.findIndex(w => w.id === walk.id); if (idx !== -1) _data[idx] = { ..._data[idx], ...updated }; UI.toast.success('Treffen aktualisiert.'); + UI.modal.close(); + _renderList(); + _renderMarkers(); } else { + if (!navigator.onLine) { + _addPending(payload); + UI.modal.close(); + UI.toast.success('Offline gespeichert — wird synchronisiert sobald Verbindung besteht.'); + _loadData(); + return; + } const created = await API.walks.create(payload); _data.unshift({ ...created, teilnehmer_count: 0 }); UI.toast.success('Treffen geplant! 🎉'); + UI.modal.close(); + _renderList(); + _renderMarkers(); } - - UI.modal.close(); - _renderList(); - _renderMarkers(); }); }); } diff --git a/backend/static/sw.js b/backend/static/sw.js index a546b58..cf7f2ba 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -119,6 +119,8 @@ const _QUEUEABLE = [ { re: /^\/api\/training\/sessions$/, methods: ['POST'] }, { re: /^\/api\/training\/progress$/, methods: ['POST'] }, { re: /^\/api\/poison$/, methods: ['POST'] }, + { re: /^\/api\/lost\/report$/, methods: ['POST'] }, + { re: /^\/api\/walks$/, methods: ['POST'] }, ]; function _isQueueable(pathname, method) { return _QUEUEABLE.some(q => q.methods.includes(method) && q.re.test(pathname)); @@ -139,6 +141,9 @@ const _CACHEABLE_GET = [ /^\/api\/wiki\/rassen/, /^\/api\/dogs\/\d+\/diary\/stats/, /^\/api\/routes$/, + /^\/api\/places$/, + /^\/api\/breeder\/map-markers$/, + /^\/api\/gassi-zeiten/, // Drei Welten — offline-fähig /^\/api\/streak\/\d+/, /^\/api\/forum\/threads/,