From be12550df180ff7319c34d04a8f7a687427462ad Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 15 May 2026 17:37:16 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20Lost-Hund=20=E2=80=94=20kein=20Doppelein?= =?UTF-8?q?trag=20nach=20Sync,=20pulsierender=20Marker,=20Verwerfen-Button?= =?UTF-8?q?,=2020km-Alert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Deduplication in _loadReports(): Pending-Einträge die bereits auf dem Server sind (Race-Condition beim Sync) werden automatisch aus dem Pending-Store entfernt - Verwerfen-Button für offline-gespeicherte Meldungen (pending), Notiz-Button nur für Server-Einträge sichtbar - Pulsierender Kreis-Marker (CSS @keyframes by-lost-ping) statt statischem Pin; Pending-Einträge in Orange, Server-Einträge in Rot - Card-Click für pending deaktiviert (kein Detail-Modal für unsynchronisierte Daten) - worlds.js: Alert-Radius für vermisste Hunde von 5 auf 20 km erhöht (wie Giftköder) - SW by-v990, APP_VER 990 --- backend/main.py | 2 +- backend/static/js/app.js | 2 +- backend/static/js/pages/lost.js | 128 +++++++++++++++++++++++--------- backend/static/js/worlds.js | 2 +- backend/static/sw.js | 2 +- 5 files changed, 97 insertions(+), 39 deletions(-) diff --git a/backend/main.py b/backend/main.py index 212bcbf..bc3f106 100644 --- a/backend/main.py +++ b/backend/main.py @@ -410,7 +410,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "989" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "990" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 4e8b66a..4cd3b61 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 = '989'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '990'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/js/pages/lost.js b/backend/static/js/pages/lost.js index 37daa9f..e4be5d8 100644 --- a/backend/static/js/pages/lost.js +++ b/backend/static/js/pages/lost.js @@ -45,14 +45,29 @@ window.Page_lost = (() => { // ---------------------------------------------------------- // MODUL-STATE // ---------------------------------------------------------- - let _container = null; - let _appState = null; - let _map = null; - let _markers = []; - let _userMarker = null; - let _reports = []; - let _userPos = null; - let _leafletLoaded = false; + let _container = null; + let _appState = null; + let _map = null; + let _markers = []; + let _userMarker = null; + let _reports = []; + let _userPos = null; + let _leafletLoaded = false; + let _stylesInjected = false; + + function _injectStyles() { + if (_stylesInjected) return; + _stylesInjected = true; + const s = document.createElement('style'); + s.textContent = ` + @keyframes by-lost-ping { + 0% { transform: scale(0.9); opacity: 0.7; } + 70% { transform: scale(2.2); opacity: 0; } + 100% { transform: scale(2.2); opacity: 0; } + } + `; + document.head.appendChild(s); + } // ---------------------------------------------------------- // INIT @@ -216,13 +231,23 @@ window.Page_lost = (() => { return; } - const pending = _getPending().map(p => ({ - ...p, - distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon), - })); try { const fetched = await API.lost.list(_userPos.lat, _userPos.lon, 25); try { localStorage.setItem(_CACHE_KEY, JSON.stringify({ ts: Date.now(), data: fetched })); } catch {} + + // Remove pending items already on the server (race: sync completed during fetch) + const rawPending = _getPending(); + const dedupedPending = rawPending.filter(p => + !fetched.some(f => f.name === p.name && + Math.abs(f.lat - p.lat) < 0.0001 && + Math.abs(f.lon - p.lon) < 0.0001) + ); + if (dedupedPending.length < rawPending.length) _setPending(dedupedPending); + + const pending = dedupedPending.map(p => ({ + ...p, + distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon), + })); _reports = [...pending, ...fetched]; _renderMarkers(); _renderHeld(); @@ -234,10 +259,15 @@ window.Page_lost = (() => { : 'Keine vermissten Hunde in deiner Nähe (25 km Radius). 🐾'; } } catch { + const offline_pending = _getPending().map(p => ({ + ...p, + distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon), + })); try { const raw = localStorage.getItem(_CACHE_KEY); if (raw) { - _reports = [...pending, ...(JSON.parse(raw).data || [])]; + const cached = JSON.parse(raw).data || []; + _reports = [...offline_pending, ...cached]; _renderMarkers(); _renderHeld(); _renderList(); @@ -246,8 +276,8 @@ window.Page_lost = (() => { return; } } catch {} - _reports = pending; - if (pending.length) { + _reports = offline_pending; + if (offline_pending.length) { _renderMarkers(); _renderHeld(); _renderList(); @@ -263,24 +293,33 @@ window.Page_lost = (() => { // ---------------------------------------------------------- function _renderMarkers() { if (!_map || !window.L) return; + _injectStyles(); _markers.forEach(m => _map.removeLayer(m)); _markers = []; _reports.forEach(r => { + const dotColor = r._isPending ? '#d97706' : '#e74c3c'; + const ringColor = r._isPending ? 'rgba(217,119,6,0.35)' : 'rgba(231,76,60,0.35)'; const icon = L.divIcon({ className : '', - html : `
🐕
`, - iconSize : [34, 34], - iconAnchor : [17, 17], + html : ` +
+
+
🐕
+
`, + iconSize : [44, 44], + iconAnchor : [22, 22], }); const distStr = r.distanz_m !== undefined - ? (r.distanz_m < 1000 ? `${r.distanz_m} m` : `${(r.distanz_m / 1000).toFixed(1)} km`) + ? (r.distanz_m < 1000 ? `${Math.round(r.distanz_m)} m` : `${(r.distanz_m / 1000).toFixed(1)} km`) : ''; const marker = L.marker([r.lat, r.lon], { icon }) @@ -289,10 +328,11 @@ window.Page_lost = (() => { 🔍 ${_escape(r.name)}
${r.rasse ? _escape(r.rasse) + '
' : ''} ${distStr ? `📍 ${distStr} entfernt
` : ''} + ${r._isPending ? '⏳ Sync ausstehend
' : ''} 📅 ${_fmtDate(r.created_at)} `); - marker.on('click', () => _openDetail(r)); + if (!r._isPending) marker.on('click', () => _openDetail(r)); _markers.push(marker); }); } @@ -334,10 +374,19 @@ window.Page_lost = (() => { listEl.innerHTML = _reports.map(r => _reportCard(r)).join(''); listEl.querySelectorAll('[data-lost-id]').forEach(card => { card.addEventListener('click', () => { - const r = _reports.find(x => x.id === parseInt(card.dataset.lostId)); + const id = card.dataset.lostId; + const r = _reports.find(x => String(x.id) === id && !x._isPending); if (r) _openDetail(r); }); }); + listEl.querySelectorAll('.lost-discard-btn').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + const pid = btn.dataset.pendingId; + _setPending(_getPending().filter(x => x.id !== pid)); + _loadReports(); + }); + }); listEl.querySelectorAll('.lost-note-btn').forEach(btn => { btn.addEventListener('click', e => { e.stopPropagation(); @@ -395,15 +444,24 @@ window.Page_lost = (() => { Gemeldet ${_fmtDate(r.created_at)} ${r.melder_name ? '· ' + _escape(r.melder_name.split(' ')[0]) : ''} - ${r._isPending ? `
⏳ Sync ausstehend
` : ''} - ${_appState.user ? `
- -
` : ''} + ${r._isPending + ? `
+ ⏳ Sync ausstehend + +
` + : (_appState.user ? `
+ +
` : '')} diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js index e343655..35cb433 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -1691,7 +1691,7 @@ window.Worlds = (() => { const pos = await API.getLocation({ timeout: 4000, maximumAge: 600_000 }); const [p, l] = await Promise.allSettled([ API.get(`/poison/nearby?lat=${pos.lat}&lon=${pos.lon}&radius=5`).catch(() => []), - API.get(`/lost/nearby?lat=${pos.lat}&lon=${pos.lon}&radius=5`).catch(() => []), + API.get(`/lost/nearby?lat=${pos.lat}&lon=${pos.lon}&radius=20`).catch(() => []), ]); if (p.value?.length) out.push({ icon:'skull', color:'#EF4444', title:'Giftköder in der Nähe', sub:`${p.value.length} Meldung${p.value.length>1?'en':''}`, page:'poison' }); if (l.value?.length) out.push({ icon:'dog', color:'#3B82F6', title:'Verlorener Hund', sub:`${l.value.length} Meldung${l.value.length>1?'en':''}`, page:'lost' }); diff --git a/backend/static/sw.js b/backend/static/sw.js index e654eab..ab84e7a 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v989'; +const CACHE_VERSION = 'by-v990'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache