diff --git a/backend/main.py b/backend/main.py index ae5e58c..a1b570f 100644 --- a/backend/main.py +++ b/backend/main.py @@ -410,7 +410,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "986" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "987" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/js/app.js b/backend/static/js/app.js index ad4311a..98b2a41 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '986'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '987'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index 80cffa9..de6fbe2 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -5,6 +5,40 @@ window.Page_routes = (() => { + const _CACHE_KEY = 'by_routes_cache'; + const _PENDING_KEY = 'by_routes_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(), user_id: null }; + 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 r of [...list]) { + try { + const { id: _pid, _isPending, ...payload } = r; + await API.routes.create(payload); + _setPending(_getPending().filter(x => x.id !== r.id)); + ok++; + } catch {} + } + if (ok > 0) { UI.toast.success(`${ok} Route(n) synchronisiert.`); _loadData(); } + } + window.addEventListener('online', _syncPending); + let _container = null; let _appState = null; let _data = []; @@ -1011,7 +1045,7 @@ window.Page_routes = (() => { const btn = document.querySelector('[form="rk-rms-form"][type="submit"]'); const fd = UI.formData(e.target); await UI.asyncButton(btn, async () => { - const saved = await API.routes.create({ + const payload = { name: fd.name?.trim(), beschreibung: fd.beschreibung || null, gps_track: track, @@ -1024,7 +1058,15 @@ window.Page_routes = (() => { is_public: 'is_public' in fd, hunde_tauglichkeit: fd.hunde_tauglichkeit || 'sehr_gut', client_time: API.clientNow(), - }); + }; + if (!navigator.onLine) { + _addPending(payload); + UI.modal.close(); + UI.toast.success(`Route offline gespeichert — wird synchronisiert sobald Verbindung besteht.`); + _loadData(); + return; + } + const saved = await API.routes.create(payload); UI.modal.close(); UI.toast.success(`Route „${saved.name}" gespeichert!`); _loadData(); @@ -1209,20 +1251,36 @@ window.Page_routes = (() => { // Daten // ---------------------------------------------------------- async function _loadData() { + const _merge = (online) => { + const pending = _getPending(); + if (pending.length) _data = [...pending, ..._data]; + if (_appState.user && _browseMode === 'mine') + document.getElementById('rk-mine-group')?.style.setProperty('display', ''); + if (_browseMode === 'discover' && _userPos) + document.getElementById('rk-nearby-group')?.style.setProperty('display', ''); + if (!online && pending.length) + UI.toast.info('Offline — ' + pending.length + ' Route(n) warten auf Sync.'); + _applyFilter(); + }; try { _data = await API.routes.list(); - // "Meine Routen"-Filter nur zeigen wenn eingeloggt und im Mine-Modus - if (_appState.user && _browseMode === 'mine') { - document.getElementById('rk-mine-group')?.style.setProperty('display', ''); - } - // Standort-abhängiger Filter im Entdecken-Modus - if (_browseMode === 'discover' && _userPos) { - document.getElementById('rk-nearby-group')?.style.setProperty('display', ''); - } - _applyFilter(); - } catch (err) { + try { localStorage.setItem(_CACHE_KEY, JSON.stringify({ ts: Date.now(), data: _data })); } catch {} + _merge(true); + } catch { + try { + const raw = localStorage.getItem(_CACHE_KEY); + if (raw) { + _data = JSON.parse(raw).data || []; + UI.toast.info('Offline — zeige zuletzt geladene Routen.'); + _merge(false); + return; + } + } catch {} + // Nur Pending-Routen zeigen wenn gar kein Cache + _data = _getPending(); + if (_data.length) { _merge(false); return; } document.getElementById('rk-grid').innerHTML = - `
Fehler: ${UI.escape(err.message)}
`; + `Offline — noch keine Routen gecacht.
`; } } @@ -1369,10 +1427,13 @@ window.Page_routes = (() => { : ''; return ` -