Feat: Routen offline aufzeichnen — LocalStorage-Queue, Cache-Fallback, Auto-Sync (SW by-v987)

This commit is contained in:
rene 2026-05-15 16:53:38 +02:00
parent 3fae57a0e2
commit 0c0daaad6b
4 changed files with 79 additions and 17 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 = "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():

View file

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

View file

@ -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 =
`<p style="color:var(--c-danger);padding:var(--space-6)">Fehler: ${UI.escape(err.message)}</p>`;
`<p style="color:var(--c-danger);padding:var(--space-6)">Offline — noch keine Routen gecacht.</p>`;
}
}
@ -1369,10 +1427,13 @@ window.Page_routes = (() => {
: '';
return `
<div class="rk-card" data-id="${r.id}">
<div class="rk-card" data-id="${r.id}" ${r._isPending ? 'data-pending="1"' : ''}>
<div class="rk-card-preview">${previewContent}</div>
<div class="rk-card-body">
${authorLine}
${r._isPending ? `<div style="font-size:10px;font-weight:700;color:var(--c-warning,#d97706);
margin-bottom:3px;display:flex;align-items:center;gap:4px">
${UI.icon('cloud-arrow-up')} Sync ausstehend</div>` : ''}
<div class="rk-card-name">${UI.escape(r.name)}</div>
<div style="display:flex;flex-wrap:wrap;gap:5px;margin:var(--space-2) 0">
${dist ? _pill(dist, 'rgba(107,114,128,0.10)','#9ca3af','rgba(107,114,128,0.30)') : ''}

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
const CACHE_VERSION = 'by-v986';
const CACHE_VERSION = 'by-v987';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
@ -138,6 +138,7 @@ const _CACHEABLE_GET = [
/^\/api\/training\/plan-progress/,
/^\/api\/wiki\/rassen/,
/^\/api\/dogs\/\d+\/diary\/stats/,
/^\/api\/routes$/,
// Drei Welten — offline-fähig
/^\/api\/streak\/\d+/,
/^\/api\/forum\/threads/,