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/,