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
This commit is contained in:
parent
f0c1ee3386
commit
be12550df1
5 changed files with 97 additions and 39 deletions
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 : `<div style="
|
||||
background:#e74c3c;color:#fff;border-radius:50%;
|
||||
width:34px;height:34px;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:17px;box-shadow:0 2px 6px rgba(0,0,0,.35);
|
||||
border:2px solid #fff">🐕</div>`,
|
||||
iconSize : [34, 34],
|
||||
iconAnchor : [17, 17],
|
||||
html : `
|
||||
<div style="position:relative;width:44px;height:44px">
|
||||
<div style="position:absolute;inset:0;border-radius:50%;
|
||||
background:${ringColor};
|
||||
animation:by-lost-ping 1.5s ease-out infinite"></div>
|
||||
<div style="position:absolute;top:5px;left:5px;
|
||||
width:34px;height:34px;
|
||||
background:${dotColor};color:#fff;border-radius:50%;
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
font-size:17px;box-shadow:0 2px 6px rgba(0,0,0,.35);
|
||||
border:2px solid #fff">🐕</div>
|
||||
</div>`,
|
||||
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 = (() => {
|
|||
<b>🔍 ${_escape(r.name)}</b><br>
|
||||
${r.rasse ? _escape(r.rasse) + '<br>' : ''}
|
||||
${distStr ? `<small>📍 ${distStr} entfernt</small><br>` : ''}
|
||||
${r._isPending ? '<small>⏳ Sync ausstehend</small><br>' : ''}
|
||||
<small>📅 ${_fmtDate(r.created_at)}</small>
|
||||
`);
|
||||
|
||||
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]) : ''}
|
||||
</div>
|
||||
${r._isPending ? `<div style="font-size:10px;color:var(--c-warning,#d97706);font-weight:600">⏳ Sync ausstehend</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>` : ''}
|
||||
${r._isPending
|
||||
? `<div style="margin-top:var(--space-2);display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap">
|
||||
<span style="font-size:10px;color:var(--c-warning,#d97706);font-weight:600">⏳ Sync ausstehend</span>
|
||||
<button class="btn btn-ghost btn-xs lost-discard-btn"
|
||||
data-pending-id="${r.id}"
|
||||
onclick="event.stopPropagation()"
|
||||
style="color:var(--c-danger,#dc2626)">
|
||||
🗑 Verwerfen
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue