diff --git a/VERSION b/VERSION
index 0f32703..0e7f8bf 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1201
\ No newline at end of file
+1203
\ No newline at end of file
diff --git a/backend/static/index.html b/backend/static/index.html
index bdb57f4..c5f862e 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 a186a68..517c478 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 = '1201'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1203'; // ← 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/map-gl-mini.js b/backend/static/js/map-gl-mini.js
index 92d07d6..e0608a5 100644
--- a/backend/static/js/map-gl-mini.js
+++ b/backend/static/js/map-gl-mini.js
@@ -115,7 +115,11 @@
_id: 'poly-' + (++_seq),
_map: null,
_opts: opts,
+ _handlers: {}, // ev → [fn]
+ _tooltip: null,
+ _tipPopup: null,
_geo: function () { return { type: 'Feature', geometry: { type: 'LineString', coordinates: this._latlngs.map(_toLngLat) } }; },
+ _hitId: function () { return this._id + '-hit'; },
_ensure: function () {
var self = this, m = self._map;
var add = function () {
@@ -123,18 +127,75 @@
if (!m.getLayer(self._id)) m.addLayer({ id: self._id, type: 'line', source: self._id,
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: { 'line-color': self._opts.color || '#C4843A', 'line-width': self._opts.weight || 4, 'line-opacity': self._opts.opacity != null ? self._opts.opacity : 0.9 } });
+ // Breite, fast unsichtbare Hit-Linie → auf dem Handy gut antippbar.
+ if (self._opts.interactive !== false && !m.getLayer(self._hitId())) {
+ m.addLayer({ id: self._hitId(), type: 'line', source: self._id,
+ layout: { 'line-cap': 'round', 'line-join': 'round' },
+ paint: { 'line-color': '#000', 'line-opacity': 0.01, 'line-width': 18 } });
+ }
+ self._wireAll();
};
if (m.isStyleLoaded && m.isStyleLoaded()) add(); else m.once('load', add);
},
+ _wireOne: function (ev, fn) {
+ var self = this, m = self._map, hit = self._hitId();
+ if (!m.getLayer(hit)) return;
+ if (ev === 'click') {
+ m.on('click', hit, function (e) { if (e.originalEvent) e.originalEvent.stopPropagation(); fn(e); });
+ } else if (ev === 'mouseover') {
+ m.on('mouseenter', hit, function (e) { m.getCanvas().style.cursor = 'pointer'; fn(e); });
+ } else if (ev === 'mouseout') {
+ m.on('mouseleave', hit, function (e) { m.getCanvas().style.cursor = ''; fn(e); });
+ }
+ },
+ _wireAll: function () {
+ var self = this;
+ Object.keys(self._handlers).forEach(function (ev) {
+ self._handlers[ev].forEach(function (fn) { self._wireOne(ev, fn); });
+ self._handlers[ev]._wired = true;
+ });
+ if (self._tooltip && !self._tipWired) self._wireTooltip();
+ },
+ _wireTooltip: function () {
+ var self = this, m = self._map, hit = self._hitId();
+ if (!m.getLayer(hit)) return;
+ self._tipWired = true;
+ m.on('mousemove', hit, function (e) {
+ if (!self._tipPopup) self._tipPopup = new maplibregl.Popup({ closeButton: false, closeOnClick: false, offset: 10, className: 'rk-map-tip' });
+ self._tipPopup.setLngLat(e.lngLat).setHTML(self._tooltip).addTo(m);
+ });
+ m.on('mouseleave', hit, function () { if (self._tipPopup) { self._tipPopup.remove(); } });
+ },
addTo: function (mapWrap) { this._map = mapWrap && mapWrap._gl ? mapWrap._gl : mapWrap; this._ensure(); return this; },
+ on: function (ev, fn) {
+ (this._handlers[ev] = this._handlers[ev] || []).push(fn);
+ if (this._map && this._map.getLayer(this._hitId())) this._wireOne(ev, fn);
+ return this;
+ },
+ bindTooltip: function (t) {
+ this._tooltip = typeof t === 'string' ? t : '';
+ if (this._map && this._map.getLayer(this._hitId())) this._wireTooltip();
+ return this;
+ },
+ setStyle: function (s) {
+ var m = this._map; if (!m || !m.getLayer(this._id)) return this;
+ if (s.color != null) m.setPaintProperty(this._id, 'line-color', s.color);
+ if (s.weight != null) m.setPaintProperty(this._id, 'line-width', s.weight);
+ if (s.opacity != null) m.setPaintProperty(this._id, 'line-opacity', s.opacity);
+ return this;
+ },
setLatLngs: function (lls) {
this._latlngs = lls || [];
if (this._map && this._map.getSource(this._id)) this._map.getSource(this._id).setData(this._geo());
return this;
},
+ // Leaflet-kompatibel: Array von {lat,lng} (für fitBounds-Sammlung).
+ getLatLngs: function () { return this._latlngs.map(function (p) { return (p && p.lat != null) ? { lat: p.lat, lng: p.lng } : { lat: p[0], lng: p[1] }; }); },
getBounds: function () { return { _coords: this._latlngs.map(function (p) { return (p && p.lat != null) ? [p.lat, p.lng] : p; }) }; },
remove: function () {
var m = this._map; if (!m) return this;
+ if (this._tipPopup) { try { this._tipPopup.remove(); } catch (e) {} }
+ if (m.getLayer(this._hitId())) m.removeLayer(this._hitId());
if (m.getLayer(this._id)) m.removeLayer(this._id);
if (m.getSource(this._id)) m.removeSource(this._id);
return this;
diff --git a/backend/static/js/pages/diary.js b/backend/static/js/pages/diary.js
index 2c553cf..d06cc5e 100644
--- a/backend/static/js/pages/diary.js
+++ b/backend/static/js/pages/diary.js
@@ -310,8 +310,10 @@ window.Page_diary = (() => {
aria-label="Schließen">×
`;
+ // #diary-list liegt in #diary-view-content (nicht direkt in _container) → vor der
+ // Liste in IHREM echten Elternknoten einfügen, sonst wirft insertBefore (NotFoundError).
const list = _container.querySelector('#diary-list');
- if (list) _container.insertBefore(card, list);
+ if (list && list.parentNode) list.parentNode.insertBefore(card, list);
card.querySelector('#diary-praise-close')?.addEventListener('click', () => {
card.style.opacity = '0';
@@ -363,6 +365,13 @@ window.Page_diary = (() => {
let _currentView = 'list'; // 'list' | 'media' | 'calendar' | 'map'
let _totalStats = null; // {entries, photos, days} — Gesamtstatistik aus API
+ let _diaryMaps = []; // aktive Karten-Instanzen → beim View-Wechsel freigeben (GL-Kontext-Leak)
+
+ // Karten beim View-Wechsel/Verlassen sauber freigeben (sonst leakt der WebGL-Kontext).
+ function _clearDiaryMaps() {
+ _diaryMaps.forEach(m => { try { m && m.remove && m.remove(); } catch (e) {} });
+ _diaryMaps = [];
+ }
async function _loadStats() {
const dog = _appState.activeDog;
@@ -431,6 +440,7 @@ window.Page_diary = (() => {
const content = _container.querySelector('#diary-view-content');
const loadMore = _container.querySelector('#diary-load-more');
if (!content) return;
+ _clearDiaryMaps(); // evtl. offene Karte (z.B. Map-Ansicht) freigeben
// "Weitere laden" nur in der Listenansicht sinnvoll
if (loadMore) loadMore.style.display = 'none';
if (_currentView === 'list') {
@@ -470,16 +480,6 @@ window.Page_diary = (() => {
return;
}
- // Leaflet laden
- if (!window.L) {
- await new Promise((res, rej) => {
- const s = document.createElement('script');
- s.src = `/js/leaflet.js?v=${APP_VER}`;
- s.onload = res; s.onerror = rej;
- document.head.appendChild(s);
- });
- }
-
const mapEl = content.querySelector('#diary-map-view');
if (!mapEl) return;
@@ -488,8 +488,23 @@ window.Page_diary = (() => {
const lons = locations.map(l => l.gps_lon);
const bounds = [[Math.min(...lats), Math.min(...lons)], [Math.max(...lats), Math.max(...lons)]];
- const map = L.map(mapEl, { zoomControl: true, attributionControl: false });
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);
+ // GL-Karte (gleicher Style wie die zentrale Karte), Fallback Leaflet über die Facade.
+ const map = await UI.map.create(mapEl, { zoomControl: true, attributionControl: false });
+ _diaryMaps.push(map);
+
+ // Popup-Klick → Eintrag öffnen (Delegation auf dem Karten-Container; engine-neutral,
+ // ersetzt das Leaflet-'popupopen'-Wiring, das die GL-Facade nicht kennt).
+ mapEl.addEventListener('click', async (e) => {
+ const pop = e.target.closest('.diary-map-popup');
+ if (!pop) return;
+ const id = parseInt(pop.dataset.id);
+ if (!_entries.find(en => en.id === id)) {
+ try { const fresh = await API.diary.get(_appState.activeDog.id, id); _entries.unshift(fresh); }
+ catch { return; }
+ }
+ if (map.closePopup) map.closePopup();
+ _openDetail(id);
+ });
// Marker für jeden Eintrag
locations.forEach(loc => {
@@ -497,59 +512,36 @@ window.Page_diary = (() => {
const dateStr = loc.datum ? new Date(loc.datum+'T12:00').toLocaleDateString('de-DE', {day:'numeric',month:'short',year:'numeric'}) : '';
const title = UI.escape(loc.titel || loc.location_name || dateStr);
- const icon = L.divIcon({
- html: hasPhoto
+ const iconHtml = hasPhoto
? `