diff --git a/backend/main.py b/backend/main.py index 212bcbf..ccf969b 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 = "991" # 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..7fdd413 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 = '991'; // ← 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..6f8fe0c 100644 --- a/backend/static/js/pages/lost.js +++ b/backend/static/js/pages/lost.js @@ -45,14 +45,32 @@ 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-pulse-r { + 0%,100% { box-shadow: 0 0 0 0 rgba(231,76,60,.55), 0 2px 6px rgba(0,0,0,.3); } + 50% { box-shadow: 0 0 0 11px rgba(231,76,60,0), 0 2px 6px rgba(0,0,0,.3); } + } + @keyframes by-lost-pulse-p { + 0%,100% { box-shadow: 0 0 0 0 rgba(217,119,6,.55), 0 2px 6px rgba(0,0,0,.3); } + 50% { box-shadow: 0 0 0 11px rgba(217,119,6,0), 0 2px 6px rgba(0,0,0,.3); } + } + `; + document.head.appendChild(s); + } // ---------------------------------------------------------- // INIT @@ -150,6 +168,7 @@ window.Page_lost = (() => { // KARTE INITIALISIEREN // ---------------------------------------------------------- function _initMap() { + _injectStyles(); const mapEl = document.getElementById('lost-map'); if (!mapEl || !window.L || _map) return; @@ -216,13 +235,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 +263,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 +280,8 @@ window.Page_lost = (() => { return; } } catch {} - _reports = pending; - if (pending.length) { + _reports = offline_pending; + if (offline_pending.length) { _renderMarkers(); _renderHeld(); _renderList(); @@ -267,20 +301,21 @@ window.Page_lost = (() => { _markers = []; _reports.forEach(r => { + const dotColor = r._isPending ? '#d97706' : '#e74c3c'; + const anim = r._isPending ? 'by-lost-pulse-p' : 'by-lost-pulse-r'; const icon = L.divIcon({ className : '', - html : `
🐕
`, + html : `
🐕
`, iconSize : [34, 34], iconAnchor : [17, 17], }); 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 +324,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 +370,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 +440,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 ? `
+ +
` : '')} @@ -414,6 +468,7 @@ window.Page_lost = (() => { // DETAIL-MODAL // ---------------------------------------------------------- function _openDetail(r) { + if (r._isPending) return; // Pending-Einträge haben keine Server-ID const isOwn = _appState.user && _appState.user.id === r.user_id; const isAdmin = _appState.user?.rolle === 'admin'; const distStr = r.distanz_m !== undefined @@ -696,19 +751,24 @@ window.Page_lost = (() => { client_time : API.clientNow(), }; - if (!navigator.onLine) { - const pending = _addPending(payload); - pending.distanz_m = _userPos - ? Math.round(_haversine(_userPos.lat, _userPos.lon, pending.lat, pending.lon)) - : 0; - UI.modal.close(); - UI.toast.success('Offline gespeichert — wird synchronisiert sobald Verbindung besteht.'); - _loadReports(); - return; + let created; + try { + created = await API.lost.report(payload); + } catch (netErr) { + // Netzwerkfehler (TypeError = fetch failed) → offline speichern + if (netErr instanceof TypeError || !navigator.onLine) { + const pending = _addPending(payload); + pending.distanz_m = _userPos + ? Math.round(_haversine(_userPos.lat, _userPos.lon, pending.lat, pending.lon)) + : 0; + UI.modal.close(); + UI.toast.success('Offline gespeichert — wird synchronisiert sobald Verbindung besteht.'); + _loadReports(); + return; + } + throw netErr; // API-Fehler (z.B. 422) → weitergeben } - const created = await API.lost.report(payload); - // Foto hochladen if (photoInput?.files[0]) { try { 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..d451c5b 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-v991'; 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