diff --git a/VERSION b/VERSION index f704a68..96bf369 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1204 \ No newline at end of file +1205 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index 9f95228..c9d56d4 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 1a4ffcf..fba73c8 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 = '1204'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1205'; // ← 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; diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index 3ac5dc6..23e3381 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -89,6 +89,7 @@ window.Page_routes = (() => { let _viewMode = 'list'; let _searchMap = null; // L.map Instanz der Suchkarte let _searchLines = new Map(); // routeId → { line, route } + let _detailMap = null; // GL-Karte im Detail-Modal (Kontext beim Schließen freigeben!) // Mini-Karten auf den Route-Cards let _miniMaps = new Map(); // routeId → L.map @@ -170,6 +171,14 @@ window.Page_routes = (() => { } function onDogChange() {} + // Beim Verlassen der Seite alle Listen-/Detail-Karten freigeben (WebGL-Kontext-Leak). + // Aktive Navigations-/Aufzeichnungs-Overlays (_navMap/_recMap) bleiben unangetastet. + function destroy() { + [_detailMap, _suggestMap, _searchMap].forEach(m => { try { m && m.remove && m.remove(); } catch (e) {} }); + _detailMap = _suggestMap = _searchMap = null; + try { _miniMaps.forEach(m => m.remove && m.remove()); _miniMaps.clear(); } catch (e) {} + } + // ---------------------------------------------------------- // Render // ---------------------------------------------------------- @@ -2434,7 +2443,12 @@ window.Page_routes = (() => { `; - UI.modal.open({ title: `🥾 ${UI.escape(route.name)}`, body, footer }); + // onClose: GL-Kontext der Detailkarte freigeben — sonst leakt jede geöffnete Route + // einen WebGL-Kontext. Nach ~8 wirft MapLibre, und UI.map.create fällt auf + // Leaflet+OSM-Raster zurück (genau das Symptom: Detailkarte plötzlich OSM-Raster + // statt GL, und der Zoom passt nicht mehr). + UI.modal.open({ title: `🥾 ${UI.escape(route.name)}`, body, footer, + onClose: () => { if (_detailMap) { try { _detailMap.remove(); } catch (e) {} _detailMap = null; } } }); UI.ratingStars({ containerId: `rk-rating-${route.id}`, @@ -2552,8 +2566,8 @@ window.Page_routes = (() => { UI.noteModal('route', route.id, label, null); }); - // Mini-Map - let _detailMap = null; + // Mini-Map (modulweite _detailMap → wird beim Schließen im onClose freigegeben) + if (_detailMap) { try { _detailMap.remove(); } catch (e) {} _detailMap = null; } setTimeout(async () => { const el = document.getElementById('rk-detail-map'); if (!el || !track.length) return; @@ -2674,21 +2688,34 @@ window.Page_routes = (() => { } } - // Karte robust auf die ganze Route fitten — auch wenn der Container beim Erstellen - // noch 0×0 ist (Modal-Animation / spätes Layout auf iOS). Feste Timeouts greifen dort - // oft zu früh; der ResizeObserver fittet erneut, SOBALD der Container seine endgültige - // Größe hat. Das war der Grund, warum die Detail-/Vorschlag-Karte auf dem Gerät beim - // Start-Zoom (zoom 14, center=Start) hängen blieb statt auf die Route zu zoomen. + // Karte robust auf die ganze Route fitten. + // WICHTIG (iOS): MapLibre verwirft ein fitBounds, das VOR dem ersten Render läuft — + // die Karte bleibt dann beim Start-Zoom (zoom 14, center=Start) hängen, statt auf die + // Route zu zoomen. (In Headless-Chromium passiert das nicht, daher fiel es dort nicht + // auf.) Deshalb fitten wir auf das 'load'/'idle'-Event der Karte — DANN ist sie wirklich + // gerendert und der Fit bleibt. Feste Timeouts + ResizeObserver als Sicherheitsnetz. function _fitRouteMap(m, el, getBounds, opts) { - opts = opts || { padding: [16, 16] }; - const fit = () => { try { m.invalidateSize(); m.fitBounds(getBounds(), opts); } catch (e) {} }; + opts = opts || { padding: [16, 16], maxZoom: 16 }; + let active = true; + const sized = () => !el || (el.clientWidth > 0 && el.clientHeight > 0); + const fit = () => { if (!active) return; try { m.invalidateSize(); m.fitBounds(getBounds(), opts); } catch (e) {} }; + const onReady = () => { + if (!active) return; + fit(); + // Erstes Ready-Event mit korrekt vermessenem Container = der gute Fit → danach Schluss, + // damit der Nutzer frei zoomen/pannen kann. + if (sized()) { active = false; try { m.off && m.off('idle', onReady); m.off && m.off('load', onReady); } catch (e) {} } + }; fit(); - setTimeout(fit, 150); setTimeout(fit, 400); + [120, 350, 700, 1200, 2000].forEach(t => setTimeout(fit, t)); + try { m.on('load', onReady); } catch (e) {} + try { m.on('idle', onReady); } catch (e) {} if (window.ResizeObserver && el) { - const ro = new ResizeObserver(() => { if (el.clientWidth > 0 && el.clientHeight > 0) fit(); }); + const ro = new ResizeObserver(() => fit()); ro.observe(el); setTimeout(() => { try { ro.disconnect(); } catch (e) {} }, 4000); } + setTimeout(() => { active = false; }, 4000); } async function _buildDetailMap(el, track) { @@ -3220,6 +3247,6 @@ window.Page_routes = (() => { // Notiz-Modal (Custom DOM — kein UI.modal um Konflikte zu vermeiden) // ---------------------------------------------------------- - return { init, refresh, onDogChange }; + return { init, refresh, onDogChange, destroy }; })(); diff --git a/backend/static/js/ui.js b/backend/static/js/ui.js index b0cd636..29d5268 100644 --- a/backend/static/js/ui.js +++ b/backend/static/js/ui.js @@ -937,7 +937,7 @@ const UI = (() => { // ---------------------------------------------------------- // TRACK-VORSCHAU-SNAPSHOT — ein Offscreen-GL-Kontext rendert PNGs (Basemap+Route) // ---------------------------------------------------------- - let _snapMap = null, _snapReady = null, _snapChain = Promise.resolve(); + let _snapMap = null, _snapReady = null, _snapChain = Promise.resolve(), _snapReleaseTimer = null; const _snapCache = new Map(); // key → data-URL const _EMPTY_FC = { type: 'FeatureCollection', features: [] }; @@ -1001,15 +1001,29 @@ const UI = (() => { })); } + // Offscreen-GL-Kontext nach Leerlauf freigeben — nicht dauerhaft halten, sonst belegt + // er einen der knappen iOS-WebGL-Kontexte und beschleunigt das Limit (Detailkarten + // fielen dann auf Leaflet+OSM-Raster zurück). Der PNG-Cache bleibt → kein Neu-Rendern. + function _releaseSnapMap() { + _snapReleaseTimer = null; + if (_snapMap) { try { _snapMap.remove(); } catch (e) {} _snapMap = null; } + _snapReady = null; + } + function _glSnapshot(track, opts = {}) { if (!_uiUseGL()) return Promise.resolve(null); // GL aus → SVG-Fallback beim Aufrufer if (!track || track.length < 2) return Promise.resolve(null); const key = opts.key || ('t' + track.length + ',' + track[0].lat + ',' + track[0].lon + ',' + track[track.length - 1].lat + ',' + track[track.length - 1].lon); if (_snapCache.has(key)) return Promise.resolve(_snapCache.get(key)); + if (_snapReleaseTimer) { clearTimeout(_snapReleaseTimer); _snapReleaseTimer = null; } // Serielle Verarbeitung am gemeinsamen Offscreen-Kontext. const run = _snapChain.then(() => _renderSnap(track, key)).catch(() => null); _snapChain = run.catch(() => {}); + run.then(() => { + if (_snapReleaseTimer) clearTimeout(_snapReleaseTimer); + _snapReleaseTimer = setTimeout(_releaseSnapMap, 15000); + }); return run; } diff --git a/backend/static/landing.html b/backend/static/landing.html index ef1a7ff..b53008f 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 6ea395b..db90cda 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 = '1204'; +const VER = '1205'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten