Feat: Routen offline aufzeichnen — LocalStorage-Queue, Cache-Fallback, Auto-Sync (SW by-v987)
This commit is contained in:
parent
3fae57a0e2
commit
0c0daaad6b
4 changed files with 79 additions and 17 deletions
|
|
@ -410,7 +410,7 @@ async def serve_media(path: str, request: _Request):
|
||||||
raise _HE(404, "Nicht gefunden.")
|
raise _HE(404, "Nicht gefunden.")
|
||||||
return _media_response(filepath)
|
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")
|
@app.get("/.well-known/assetlinks.json")
|
||||||
async def assetlinks():
|
async def assetlinks():
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
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 APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
|
||||||
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
const IS_STAGING = location.hostname === 'staging.banyaro.app';
|
||||||
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
// Cache-Bust-Parameter nach Update-Reload sofort entfernen
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,40 @@
|
||||||
|
|
||||||
window.Page_routes = (() => {
|
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 _container = null;
|
||||||
let _appState = null;
|
let _appState = null;
|
||||||
let _data = [];
|
let _data = [];
|
||||||
|
|
@ -1011,7 +1045,7 @@ window.Page_routes = (() => {
|
||||||
const btn = document.querySelector('[form="rk-rms-form"][type="submit"]');
|
const btn = document.querySelector('[form="rk-rms-form"][type="submit"]');
|
||||||
const fd = UI.formData(e.target);
|
const fd = UI.formData(e.target);
|
||||||
await UI.asyncButton(btn, async () => {
|
await UI.asyncButton(btn, async () => {
|
||||||
const saved = await API.routes.create({
|
const payload = {
|
||||||
name: fd.name?.trim(),
|
name: fd.name?.trim(),
|
||||||
beschreibung: fd.beschreibung || null,
|
beschreibung: fd.beschreibung || null,
|
||||||
gps_track: track,
|
gps_track: track,
|
||||||
|
|
@ -1024,7 +1058,15 @@ window.Page_routes = (() => {
|
||||||
is_public: 'is_public' in fd,
|
is_public: 'is_public' in fd,
|
||||||
hunde_tauglichkeit: fd.hunde_tauglichkeit || 'sehr_gut',
|
hunde_tauglichkeit: fd.hunde_tauglichkeit || 'sehr_gut',
|
||||||
client_time: API.clientNow(),
|
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.modal.close();
|
||||||
UI.toast.success(`Route „${saved.name}" gespeichert!`);
|
UI.toast.success(`Route „${saved.name}" gespeichert!`);
|
||||||
_loadData();
|
_loadData();
|
||||||
|
|
@ -1209,20 +1251,36 @@ window.Page_routes = (() => {
|
||||||
// Daten
|
// Daten
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
async function _loadData() {
|
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 {
|
try {
|
||||||
_data = await API.routes.list();
|
_data = await API.routes.list();
|
||||||
// "Meine Routen"-Filter nur zeigen wenn eingeloggt und im Mine-Modus
|
try { localStorage.setItem(_CACHE_KEY, JSON.stringify({ ts: Date.now(), data: _data })); } catch {}
|
||||||
if (_appState.user && _browseMode === 'mine') {
|
_merge(true);
|
||||||
document.getElementById('rk-mine-group')?.style.setProperty('display', '');
|
} catch {
|
||||||
}
|
try {
|
||||||
// Standort-abhängiger Filter im Entdecken-Modus
|
const raw = localStorage.getItem(_CACHE_KEY);
|
||||||
if (_browseMode === 'discover' && _userPos) {
|
if (raw) {
|
||||||
document.getElementById('rk-nearby-group')?.style.setProperty('display', '');
|
_data = JSON.parse(raw).data || [];
|
||||||
}
|
UI.toast.info('Offline — zeige zuletzt geladene Routen.');
|
||||||
_applyFilter();
|
_merge(false);
|
||||||
} catch (err) {
|
return;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
// Nur Pending-Routen zeigen wenn gar kein Cache
|
||||||
|
_data = _getPending();
|
||||||
|
if (_data.length) { _merge(false); return; }
|
||||||
document.getElementById('rk-grid').innerHTML =
|
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 `
|
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-preview">${previewContent}</div>
|
||||||
<div class="rk-card-body">
|
<div class="rk-card-body">
|
||||||
${authorLine}
|
${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 class="rk-card-name">${UI.escape(r.name)}</div>
|
||||||
<div style="display:flex;flex-wrap:wrap;gap:5px;margin:var(--space-2) 0">
|
<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)') : ''}
|
${dist ? _pill(dist, 'rgba(107,114,128,0.10)','#9ca3af','rgba(107,114,128,0.30)') : ''}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Offline-Cache + Push Notifications + Tile-Cache
|
Offline-Cache + Push Notifications + Tile-Cache
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const CACHE_VERSION = 'by-v986';
|
const CACHE_VERSION = 'by-v987';
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache
|
||||||
|
|
@ -138,6 +138,7 @@ const _CACHEABLE_GET = [
|
||||||
/^\/api\/training\/plan-progress/,
|
/^\/api\/training\/plan-progress/,
|
||||||
/^\/api\/wiki\/rassen/,
|
/^\/api\/wiki\/rassen/,
|
||||||
/^\/api\/dogs\/\d+\/diary\/stats/,
|
/^\/api\/dogs\/\d+\/diary\/stats/,
|
||||||
|
/^\/api\/routes$/,
|
||||||
// Drei Welten — offline-fähig
|
// Drei Welten — offline-fähig
|
||||||
/^\/api\/streak\/\d+/,
|
/^\/api\/streak\/\d+/,
|
||||||
/^\/api\/forum\/threads/,
|
/^\/api\/forum\/threads/,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue