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