From 78866206b4d06c50b74d1d923bbc8488a2940676 Mon Sep 17 00:00:00 2001 From: rene Date: Thu, 4 Jun 2026 17:13:23 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20Routenaufzeichnung=20=C3=BCbersteht?= =?UTF-8?q?=20App-Updates=20(Guard=20+=20Persistenz)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- VERSION | 2 +- backend/static/index.html | 24 +++++------ backend/static/js/app.js | 5 ++- backend/static/js/boot.js | 46 +++++++++++++++------ backend/static/js/pages/map.js | 67 ++++++++++++++++++++++++++++--- backend/static/js/pages/routes.js | 66 +++++++++++++++++++++++++++--- backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- 8 files changed, 174 insertions(+), 40 deletions(-) diff --git a/VERSION b/VERSION index 913cc6a..805f574 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1166 \ No newline at end of file +1167 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index b8fdbb1..4ab7285 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -617,11 +617,11 @@ - - - - - + + + + + @@ -631,7 +631,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 2b173b4..5515a3a 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 = '1166'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1167'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator) window.APP_VERSION = APP_VERSION; @@ -137,7 +137,8 @@ const App = (() => { let lastForce = 0; try { lastForce = parseInt(localStorage.getItem('by_last_force_update') || '0', 10); } catch {} const cooldownActive = (Date.now() - lastForce) < 10 * 60 * 1000; - if (!modalOpen && !cooldownActive) { + // Während einer laufenden Aufzeichnung NIE force-updaten (Datenverlust). + if (!modalOpen && !cooldownActive && !window._byRecording) { window._byUpdatePending = false; sessionStorage.setItem('by_updated_to', window._byNewVersion || ''); sessionStorage.setItem('by_update_target', pageId); diff --git a/backend/static/js/boot.js b/backend/static/js/boot.js index d1ef297..50d5352 100644 --- a/backend/static/js/boot.js +++ b/backend/static/js/boot.js @@ -46,6 +46,38 @@ _updateBanner(); })(); +// ---------------------------------------------------------- +// Aufzeichnungs-Speicher (Sicherheitsnetz gegen Datenverlust bei Reload/Crash) +// ---------------------------------------------------------- +window.RecStore = { + save: function(s) { try { localStorage.setItem('by_active_recording', JSON.stringify(Object.assign({ ts: Date.now() }, s))); } catch (e) {} }, + load: function() { try { return JSON.parse(localStorage.getItem('by_active_recording') || 'null'); } catch (e) { return null; } }, + clear: function() { try { localStorage.removeItem('by_active_recording'); } catch (e) {} }, +}; + +// ---------------------------------------------------------- +// SW-Reload — wird während einer laufenden Routen-Aufzeichnung AUFGESCHOBEN, +// damit der nur im RAM gehaltene Track nicht verloren geht. Sobald die +// Aufzeichnung beendet ist, holt window._byReloadIfPending() den Reload nach. +// ---------------------------------------------------------- +window._byReloadIfPending = function() { + if (window._byReloadPending) { + window._byReloadPending = false; + window.location.replace('/?_t=' + Date.now()); + } +}; +function _bySwReload() { + if (sessionStorage.getItem('by_skip_sw_reload')) { + sessionStorage.removeItem('by_skip_sw_reload'); // einmalig konsumieren + return; + } + if (window._byRecording) { // Aufzeichnung läuft → Reload aufschieben + window._byReloadPending = true; + return; + } + window.location.replace('/?_t=' + Date.now()); +} + // ---------------------------------------------------------- // Service Worker Registration + Update-Flow // ---------------------------------------------------------- @@ -56,13 +88,7 @@ if ('serviceWorker' in navigator) { function _watchSW(sw) { if (!sw) return; sw.addEventListener('statechange', function() { - if (sw.state === 'activated') { - if (sessionStorage.getItem('by_skip_sw_reload')) { - sessionStorage.removeItem('by_skip_sw_reload'); // einmalig konsumieren - return; - } - window.location.replace('/?_t=' + Date.now()); - } + if (sw.state === 'activated') _bySwReload(); }); } reg.addEventListener('updatefound', function() { _watchSW(reg.installing); }); @@ -83,11 +109,7 @@ if ('serviceWorker' in navigator) { // NICHT registrieren wenn diese Seite selbst durch SW-Reload entstand if (!window._BY_SW_RELOAD) { navigator.serviceWorker.addEventListener('controllerchange', function() { - if (sessionStorage.getItem('by_skip_sw_reload')) { - sessionStorage.removeItem('by_skip_sw_reload'); - return; - } - window.location.replace('/?_t=' + Date.now()); + _bySwReload(); }); } diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index 2d9dac0..10b30fd 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -156,6 +156,7 @@ window.Page_map = (() => { _initMap(); // sofort mit Deutschland-Mitte starten _startLocationTracking(); _loadAll(); + _offerResume(); // unterbrochene Aufzeichnung anbieten // Standort im Hintergrund holen — bei Erfolg zur Position fliegen API.getLocation().then(pos => { _userPos = pos; @@ -1726,7 +1727,45 @@ window.Page_map = (() => { else _stopRecording(); } - async function _startRecording() { + // Aufzeichnung gedrosselt nach localStorage sichern (Sicherheitsnetz gegen + // Datenverlust bei Reload/Crash). force=true schreibt sofort. + let _recPersistAt = 0; + function _persistRec(force) { + const now = Date.now(); + if (!force && now - _recPersistAt < 8000) return; + _recPersistAt = now; + window.RecStore?.save({ source: 'map', track: _recTrack, distKm: _recDistKm, startTime: _recStartTime }); + } + + // Aufzeichnung endgültig abgeschlossen (gespeichert/verworfen): Speicher + // leeren, Guard lösen und einen ggf. aufgeschobenen Update-Reload nachholen. + function _recDone() { + window.RecStore?.clear(); + window._byRecording = false; + window._byReloadIfPending?.(); + } + + // Unterbrochene Aufzeichnung (Reload/Crash/Update) zum Fortsetzen anbieten. + let _resumeOffered = false; + async function _offerResume() { + if (_recActive || _resumeOffered) return; + const saved = window.RecStore?.load(); + if (!saved || saved.source !== 'map' || !Array.isArray(saved.track) || saved.track.length < 2) return; + if (Date.now() - (saved.ts || 0) > 6 * 3600 * 1000) { window.RecStore?.clear(); return; } // > 6h alt + _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', + }); + // Nur explizites Fortsetzen resumt; sonst Track behalten (erneut anbieten / + // Staleness räumt nach 6h auf) — kein versehentlicher Datenverlust. + if (ok) _startRecording(saved); + } + + async function _startRecording(resume) { if (!_appState.user) { UI.toast.warning('Bitte zuerst anmelden.'); App.navigate('settings'); @@ -1736,11 +1775,12 @@ window.Page_map = (() => { UI.toast.error('GPS nicht verfügbar.'); return; } + window._byRecording = true; // Guard: Update-Reload wird aufgeschoben _recActive = true; _recPaused = false; - _recTrack = []; - _recDistKm = 0; - _recStartTime = Date.now(); + _recTrack = (resume && Array.isArray(resume.track)) ? resume.track.slice() : []; + _recDistKm = resume?.distKm || 0; + _recStartTime = resume?.startTime || Date.now(); // FAB umschalten const btn = document.getElementById('map-rec-btn'); @@ -1775,13 +1815,24 @@ window.Page_map = (() => { _recDistKm += d / 1000; } _recTrack.push({ lat, lon }); + _persistRec(); _updateRecMap(lat, lon); _updateRecStatus(); }, () => {}, { enableHighAccuracy: true, maximumAge: 0, timeout: 10000 } ); - UI.toast.success('Aufzeichnung gestartet — los geht\'s!'); + + // Fortgesetzte Aufzeichnung: bestehenden Track sofort einzeichnen + if (resume && _recTrack.length && _map && window.L) { + _recPolyline = L.polyline(_recTrack.map(p => [p.lat, p.lon]), { color: '#EF4444', weight: 5, opacity: 0.9 }).addTo(_map); + const last = _recTrack[_recTrack.length - 1]; + _recMarker = L.circleMarker([last.lat, last.lon], { radius: 8, color: '#EF4444', fillColor: '#fff', fillOpacity: 1, weight: 3 }).addTo(_map); + _map.panTo([last.lat, last.lon]); + _updateRecStatus(); + } + _persistRec(true); + UI.toast.success(resume ? 'Aufzeichnung fortgesetzt.' : 'Aufzeichnung gestartet — los geht\'s!'); // Pocket-Modus aktivieren wenn in Einstellungen eingeschaltet if (localStorage.getItem('by_pocket_mode') === 'true') { @@ -1882,9 +1933,13 @@ window.Page_map = (() => { UI.toast.warning('Zu wenige GPS-Punkte — bitte etwas länger laufen.'); if (_recPolyline) { _recPolyline.remove(); _recPolyline = null; } if (_recMarker) { _recMarker.remove(); _recMarker = null; } + _recDone(); return; } + // Guard bleibt aktiv bis gespeichert/verworfen — der Track liegt jetzt im + // Save-Modal UND (als Netz) in RecStore. + _persistRec(true); const dauMin = Math.max(1, Math.floor((Date.now() - _recStartTime) / 1000 / 60)); _showRecSaveModal(_recTrack, _recDistKm, dauMin); } @@ -2013,6 +2068,7 @@ window.Page_map = (() => { UI.modal.close(); if (_recPolyline) { _recPolyline.remove(); _recPolyline = null; } if (_recMarker) { _recMarker.remove(); _recMarker = null; } + _recDone(); }); // Hund-Checkbox Toggle-Styling @@ -2050,6 +2106,7 @@ window.Page_map = (() => { UI.modal.close(); if (_recPolyline) { _recPolyline.remove(); _recPolyline = null; } if (_recMarker) { _recMarker.remove(); _recMarker = null; } + _recDone(); if (saved.is_valid === false) { UI.toast.warning(`Route „${saved.name}" gespeichert — wird nicht für Statistiken gewertet (Geschwindigkeit zu hoch).`); } else { diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index 10fc523..b5952e3 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -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(); }); diff --git a/backend/static/landing.html b/backend/static/landing.html index 953a021..fb597b4 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -4,7 +4,7 @@ - + Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz diff --git a/backend/static/sw.js b/backend/static/sw.js index 49e565e..245c359 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -4,7 +4,7 @@ ============================================================ */ // ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab -const VER = '1166'; +const VER = '1167'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten