Compare commits

...

2 commits

Author SHA1 Message Date
f2856b8acb Fix: Lost — Puls-Animation (box-shadow), false-offline, Pending-Guard
- Pulsierender Marker: Wechsel von position:absolute-Ring auf box-shadow-Animation
  (by-lost-pulse-r/p), kein Overflow-Problem mit Leaflet divIcon, iOS-kompatibel
- navigator.onLine iOS-Falsch-Positiv: Formular-Submit versucht API zuerst,
  fällt nur bei TypeError (fetch failed) auf Pending-Modus zurück
- _openDetail(): früher Return für Pending-Einträge (verhindert delete mit
  string-ID "pending_..." → Backend-Fehler "unable to parse integer")
- SW by-v991, APP_VER 991
2026-05-15 17:44:59 +02:00
be12550df1 Fix: Lost-Hund — kein Doppeleintrag nach Sync, pulsierender Marker, Verwerfen-Button, 20km-Alert
- 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
2026-05-15 17:37:16 +02:00
5 changed files with 108 additions and 48 deletions

View file

@ -410,7 +410,7 @@ async def serve_media(path: str, request: _Request):
raise _HE(404, "Nicht gefunden.") raise _HE(404, "Nicht gefunden.")
return _media_response(filepath) 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") @app.get("/.well-known/assetlinks.json")
async def assetlinks(): async def assetlinks():

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung. 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 APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app'; const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen // Cache-Bust-Parameter nach Update-Reload sofort entfernen

View file

@ -45,14 +45,32 @@ window.Page_lost = (() => {
// ---------------------------------------------------------- // ----------------------------------------------------------
// MODUL-STATE // MODUL-STATE
// ---------------------------------------------------------- // ----------------------------------------------------------
let _container = null; let _container = null;
let _appState = null; let _appState = null;
let _map = null; let _map = null;
let _markers = []; let _markers = [];
let _userMarker = null; let _userMarker = null;
let _reports = []; let _reports = [];
let _userPos = null; let _userPos = null;
let _leafletLoaded = false; 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 // INIT
@ -150,6 +168,7 @@ window.Page_lost = (() => {
// KARTE INITIALISIEREN // KARTE INITIALISIEREN
// ---------------------------------------------------------- // ----------------------------------------------------------
function _initMap() { function _initMap() {
_injectStyles();
const mapEl = document.getElementById('lost-map'); const mapEl = document.getElementById('lost-map');
if (!mapEl || !window.L || _map) return; if (!mapEl || !window.L || _map) return;
@ -216,13 +235,23 @@ window.Page_lost = (() => {
return; return;
} }
const pending = _getPending().map(p => ({
...p,
distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon),
}));
try { try {
const fetched = await API.lost.list(_userPos.lat, _userPos.lon, 25); const fetched = await API.lost.list(_userPos.lat, _userPos.lon, 25);
try { localStorage.setItem(_CACHE_KEY, JSON.stringify({ ts: Date.now(), data: fetched })); } catch {} 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]; _reports = [...pending, ...fetched];
_renderMarkers(); _renderMarkers();
_renderHeld(); _renderHeld();
@ -234,10 +263,15 @@ window.Page_lost = (() => {
: 'Keine vermissten Hunde in deiner Nähe (25 km Radius). 🐾'; : 'Keine vermissten Hunde in deiner Nähe (25 km Radius). 🐾';
} }
} catch { } catch {
const offline_pending = _getPending().map(p => ({
...p,
distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon),
}));
try { try {
const raw = localStorage.getItem(_CACHE_KEY); const raw = localStorage.getItem(_CACHE_KEY);
if (raw) { if (raw) {
_reports = [...pending, ...(JSON.parse(raw).data || [])]; const cached = JSON.parse(raw).data || [];
_reports = [...offline_pending, ...cached];
_renderMarkers(); _renderMarkers();
_renderHeld(); _renderHeld();
_renderList(); _renderList();
@ -246,8 +280,8 @@ window.Page_lost = (() => {
return; return;
} }
} catch {} } catch {}
_reports = pending; _reports = offline_pending;
if (pending.length) { if (offline_pending.length) {
_renderMarkers(); _renderMarkers();
_renderHeld(); _renderHeld();
_renderList(); _renderList();
@ -267,20 +301,21 @@ window.Page_lost = (() => {
_markers = []; _markers = [];
_reports.forEach(r => { _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({ const icon = L.divIcon({
className : '', className : '',
html : `<div style=" html : `<div style="background:${dotColor};color:#fff;border-radius:50%;
background:#e74c3c;color:#fff;border-radius:50%; width:34px;height:34px;
width:34px;height:34px; display:flex;align-items:center;justify-content:center;
display:flex;align-items:center;justify-content:center; font-size:17px;border:2px solid #fff;
font-size:17px;box-shadow:0 2px 6px rgba(0,0,0,.35); animation:${anim} 1.8s ease-in-out infinite">🐕</div>`,
border:2px solid #fff">🐕</div>`,
iconSize : [34, 34], iconSize : [34, 34],
iconAnchor : [17, 17], iconAnchor : [17, 17],
}); });
const distStr = r.distanz_m !== undefined 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 }) const marker = L.marker([r.lat, r.lon], { icon })
@ -289,10 +324,11 @@ window.Page_lost = (() => {
<b>🔍 ${_escape(r.name)}</b><br> <b>🔍 ${_escape(r.name)}</b><br>
${r.rasse ? _escape(r.rasse) + '<br>' : ''} ${r.rasse ? _escape(r.rasse) + '<br>' : ''}
${distStr ? `<small>📍 ${distStr} entfernt</small><br>` : ''} ${distStr ? `<small>📍 ${distStr} entfernt</small><br>` : ''}
${r._isPending ? '<small>⏳ Sync ausstehend</small><br>' : ''}
<small>📅 ${_fmtDate(r.created_at)}</small> <small>📅 ${_fmtDate(r.created_at)}</small>
`); `);
marker.on('click', () => _openDetail(r)); if (!r._isPending) marker.on('click', () => _openDetail(r));
_markers.push(marker); _markers.push(marker);
}); });
} }
@ -334,10 +370,19 @@ window.Page_lost = (() => {
listEl.innerHTML = _reports.map(r => _reportCard(r)).join(''); listEl.innerHTML = _reports.map(r => _reportCard(r)).join('');
listEl.querySelectorAll('[data-lost-id]').forEach(card => { listEl.querySelectorAll('[data-lost-id]').forEach(card => {
card.addEventListener('click', () => { 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); 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 => { listEl.querySelectorAll('.lost-note-btn').forEach(btn => {
btn.addEventListener('click', e => { btn.addEventListener('click', e => {
e.stopPropagation(); e.stopPropagation();
@ -395,15 +440,24 @@ window.Page_lost = (() => {
Gemeldet ${_fmtDate(r.created_at)} Gemeldet ${_fmtDate(r.created_at)}
${r.melder_name ? '· ' + _escape(r.melder_name.split(' ')[0]) : ''} ${r.melder_name ? '· ' + _escape(r.melder_name.split(' ')[0]) : ''}
</div> </div>
${r._isPending ? `<div style="font-size:10px;color:var(--c-warning,#d97706);font-weight:600">⏳ Sync ausstehend</div>` : ''} ${r._isPending
${_appState.user ? `<div style="margin-top:var(--space-2)"> ? `<div style="margin-top:var(--space-2);display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
<button class="btn btn-ghost btn-xs lost-note-btn" <span style="font-size:10px;color:var(--c-warning,#d97706);font-weight:600"> Sync ausstehend</span>
data-lost-note-id="${r.id}" <button class="btn btn-ghost btn-xs lost-discard-btn"
data-lost-note-name="${_escape(r.name)}" data-pending-id="${r.id}"
title="Notiz" onclick="event.stopPropagation()"> onclick="event.stopPropagation()"
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz style="color:var(--c-danger,#dc2626)">
</button> 🗑 Verwerfen
</div>` : ''} </button>
</div>`
: (_appState.user ? `<div style="margin-top:var(--space-2)">
<button class="btn btn-ghost btn-xs lost-note-btn"
data-lost-note-id="${r.id}"
data-lost-note-name="${_escape(r.name)}"
title="Notiz" onclick="event.stopPropagation()">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#note-pencil"></use></svg> Notiz
</button>
</div>` : '')}
</div> </div>
</div> </div>
</div> </div>
@ -414,6 +468,7 @@ window.Page_lost = (() => {
// DETAIL-MODAL // DETAIL-MODAL
// ---------------------------------------------------------- // ----------------------------------------------------------
function _openDetail(r) { 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 isOwn = _appState.user && _appState.user.id === r.user_id;
const isAdmin = _appState.user?.rolle === 'admin'; const isAdmin = _appState.user?.rolle === 'admin';
const distStr = r.distanz_m !== undefined const distStr = r.distanz_m !== undefined
@ -696,19 +751,24 @@ window.Page_lost = (() => {
client_time : API.clientNow(), client_time : API.clientNow(),
}; };
if (!navigator.onLine) { let created;
const pending = _addPending(payload); try {
pending.distanz_m = _userPos created = await API.lost.report(payload);
? Math.round(_haversine(_userPos.lat, _userPos.lon, pending.lat, pending.lon)) } catch (netErr) {
: 0; // Netzwerkfehler (TypeError = fetch failed) → offline speichern
UI.modal.close(); if (netErr instanceof TypeError || !navigator.onLine) {
UI.toast.success('Offline gespeichert — wird synchronisiert sobald Verbindung besteht.'); const pending = _addPending(payload);
_loadReports(); pending.distanz_m = _userPos
return; ? 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 // Foto hochladen
if (photoInput?.files[0]) { if (photoInput?.files[0]) {
try { try {

View file

@ -1691,7 +1691,7 @@ window.Worlds = (() => {
const pos = await API.getLocation({ timeout: 4000, maximumAge: 600_000 }); const pos = await API.getLocation({ timeout: 4000, maximumAge: 600_000 });
const [p, l] = await Promise.allSettled([ const [p, l] = await Promise.allSettled([
API.get(`/poison/nearby?lat=${pos.lat}&lon=${pos.lon}&radius=5`).catch(() => []), 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 (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' }); 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' });

View file

@ -3,7 +3,7 @@
Offline-Cache + Push Notifications + Tile-Cache Offline-Cache + Push Notifications + Tile-Cache
============================================================ */ ============================================================ */
const CACHE_VERSION = 'by-v989'; const CACHE_VERSION = 'by-v991';
const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache