Feature: Routenaufzeichnung übersteht App-Updates (Guard + Persistenz)

Stufe 1 (Guard): Während aktiver Aufzeichnung wird der SW-/Force-Update-Reload
aufgeschoben (window._byRecording → boot.js/_bySwReload + app.js force-update);
nach Stop/Speichern via window._byReloadIfPending() nachgeholt.

Stufe 2 (Persistenz): Track wird gedrosselt nach localStorage (RecStore) gesichert
und beim nächsten Öffnen der Karten-/Routen-Seite als 'Aufzeichnung fortsetzen?'
angeboten (Resume seedet Track+km+Startzeit). Schützt auch bei Crash/OS-Kill/
manuellem Reload. Greift in map.js UND routes.js. SW v1167
This commit is contained in:
rene 2026-06-04 17:13:23 +02:00
parent ddfb9474ef
commit 78866206b4
8 changed files with 174 additions and 40 deletions

View file

@ -127,6 +127,7 @@ window.Page_routes = (() => {
_flushPendingNavWalk(); // nicht gespeicherten Navigations-Walk nachtragen
try { _userPos = await API.getLocation(); } catch {}
await _loadData();
_offerResume(); // unterbrochene Aufzeichnung anbieten
// Vorschlag sofort rendern (Leaflet war noch nicht bereit bei _render)
if (params._suggestResult) {
@ -659,6 +660,26 @@ window.Page_routes = (() => {
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
// Unterbrochene Aufzeichnung (Reload/Crash/Update) zum Fortsetzen anbieten.
let _resumeOffered = false;
async function _offerResume() {
if (_recActive || _resumeOffered || _recOvl) return;
const saved = window.RecStore?.load();
if (!saved || saved.source !== 'routes' || !Array.isArray(saved.track) || saved.track.length < 2) return;
if (Date.now() - (saved.ts || 0) > 6 * 3600 * 1000) { window.RecStore?.clear(); return; }
_resumeOffered = true;
const km = (saved.distKm || 0).toFixed(2);
const ok = await UI.modal.confirm({
title: 'Aufzeichnung fortsetzen?',
message: `Eine unterbrochene Aufzeichnung wurde gefunden (${km} km, ${saved.track.length} Punkte). Möchtest du sie fortsetzen?`,
confirmText: 'Fortsetzen',
cancelText: 'Später',
});
if (!ok) return; // Track bleibt erhalten (erneut anbieten / Staleness räumt auf)
await _openRecOvl();
await _startRecInOvl(saved);
}
async function _openRecOvl() {
if (!_appState.user) { UI.toast.warning('Bitte anmelden.'); return; }
if (_recOvl) return;
@ -752,10 +773,30 @@ window.Page_routes = (() => {
} catch {}
}
async function _startRecInOvl() {
// Aufzeichnung gedrosselt sichern (Sicherheitsnetz gegen Datenverlust).
let _recPersistAt = 0;
function _persistRec(force) {
const now = Date.now();
if (!force && now - _recPersistAt < 8000) return;
_recPersistAt = now;
window.RecStore?.save({ source: 'routes', track: _recTrack, distKm: _recDistKm, startTime: _recStartTime });
}
function _recDone() {
window.RecStore?.clear();
window._byRecording = false;
window._byReloadIfPending?.();
}
async function _startRecInOvl(resume) {
if (!navigator.geolocation) { UI.toast.error('GPS nicht verfügbar.'); return; }
window._byRecording = true; // Guard: Update-Reload wird aufgeschoben
_recActive = true;
_recTrack = []; _recDistKm = 0; _recStartTime = Date.now();
if (resume && Array.isArray(resume.track) && resume.track.length) {
_recTrack = resume.track.slice(); _recDistKm = resume.distKm || 0;
_recStartTime = resume.startTime || Date.now();
} else {
_recTrack = []; _recDistKm = 0; _recStartTime = Date.now();
}
// iOS-Hinweis: Display muss wach bleiben
if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
@ -816,8 +857,16 @@ window.Page_routes = (() => {
document.getElementById('rk-rec-stats-bar').style.display = '';
if (_recMap && window.L) {
_recPolyline = L.polyline([], { color: '#ef4444', weight: 5, opacity: 0.9 }).addTo(_recMap);
// Bei Fortsetzung den bestehenden Track sofort einzeichnen
const seed = (resume && _recTrack.length) ? _recTrack.map(p => [p.lat, p.lon]) : [];
_recPolyline = L.polyline(seed, { color: '#ef4444', weight: 5, opacity: 0.9 }).addTo(_recMap);
if (seed.length) {
const last = seed[seed.length - 1];
_recLocMarker?.setLatLng(last);
_recMap.setView(last, 16);
}
}
if (resume) { _updateRecStats(); _persistRec(true); }
await _recAcquireWakeLock();
document.addEventListener('visibilitychange', _recOnVisibility);
@ -832,6 +881,7 @@ window.Page_routes = (() => {
_recDistKm += d;
}
_recTrack.push({ lat, lon, ...(alt !== null ? { alt: Math.round(alt) } : {}) });
_persistRec();
_recPolyline?.addLatLng([lat, lon]);
_recLocMarker?.setLatLng([lat, lon]);
if (_recTrack.length === 1) _recMap?.setView([lat, lon], 16);
@ -940,12 +990,14 @@ window.Page_routes = (() => {
_recOvl?.removeEventListener('touchstart', _onRecOvlTouch);
_recOvl?.removeEventListener('pointerdown', _onRecOvlTouch);
if (!save) { _closeRecOvlClean(); return; }
if (!save) { _closeRecOvlClean(); _recDone(); return; }
const track = [..._recTrack], distKm = _recDistKm;
const dauMin = Math.round((Date.now() - _recStartTime) / 60000);
_persistRec(true); // finalen Stand sichern, bevor _recTrack zurückgesetzt wird
_closeRecOvlClean();
if (track.length < 2) { UI.toast.warning('Zu wenige GPS-Punkte zum Speichern.'); return; }
if (track.length < 2) { UI.toast.warning('Zu wenige GPS-Punkte zum Speichern.'); _recDone(); return; }
// Guard bleibt aktiv bis im Save-Modal gespeichert/verworfen wird.
_showRecSaveModal(track, distKm, dauMin);
}
@ -1048,7 +1100,7 @@ window.Page_routes = (() => {
document.getElementById('rk-rms-paw-val').value = btn.dataset.val;
});
document.getElementById('rk-rms-discard')?.addEventListener('click', () => UI.modal.close());
document.getElementById('rk-rms-discard')?.addEventListener('click', () => { UI.modal.close(); _recDone(); });
document.getElementById('rk-rms-form')?.addEventListener('submit', async e => {
e.preventDefault();
@ -1072,12 +1124,14 @@ window.Page_routes = (() => {
if (!navigator.onLine) {
_addPending(payload);
UI.modal.close();
_recDone();
UI.toast.success(`Route offline gespeichert — wird synchronisiert sobald Verbindung besteht.`);
_loadData();
return;
}
const saved = await API.routes.create(payload);
UI.modal.close();
_recDone();
UI.toast.success(`Route „${saved.name}" gespeichert!`);
_loadData();
});