Sprint 10: OSM-POI-Cache, Karten-Clustering, Routen-Redesign

Karte (map.js):
- OSM Overpass API: Restaurants, Tierärzte, Parkplätze, Bänke, Wasserstellen
- Leaflet.markercluster für alle OSM-Layer
- Standort-Dot mit GPS-Genauigkeitskreis, Wake-Lock bei Aufzeichnung
- Community-Pins setzen/löschen, Meldungen, Crosshair-Placement
- Layer-Sichtbarkeit in localStorage (by_map_visible_v1)

Routen (routes.js + routen.py):
- Komoot-Stil: SVG-Track-Preview, Foto-Upload, Nearby-POIs im Detail-Modal
- Neue Felder: is_public, hunde_tauglichkeit, foto_urls
- Rate-Endpoint (POST /api/routes/{id}/rate)
- Foto-Upload (POST /api/routes/{id}/photo)
- Fix: json_extract $[-1] → $[#-1] (SQLite-kompatibler Pfad für letztes Element)

Backend (osm.py, database.py, scheduler.py):
- /api/osm/pois: OSM-Overpass-Cache mit Tile-Logik (14 Tage TTL)
- /api/osm/user-poi: Community-Marker CRUD
- /api/osm/report: Marker als ungültig melden
- Neue Tabellen: osm_pois, osm_tiles, user_map_pois, osm_reports
- Giftköder-Archiv-Job (täglich 03:00, soft-delete nach Ablauf)
- Giftköder-Archiv-Job als APScheduler-CronJob

UI: Orte-Menüpunkt entfernt (in Karte integriert), APP_VER auf 62
This commit is contained in:
rene 2026-04-15 16:30:10 +02:00
parent bf26e5faf4
commit ebe4ce20cf
16 changed files with 3020 additions and 737 deletions

View file

@ -188,6 +188,14 @@ const API = (() => {
create(data) { return post('/routes', data); },
update(id, data) { return patch(`/routes/${id}`, data); },
delete(id) { return del(`/routes/${id}`); },
rate(id, wertung) { return post(`/routes/${id}/rate`, { wertung }); },
addPhoto(id, file) {
const fd = new FormData();
fd.append('file', file);
return fetch(`/api/routes/${id}/photo`, {
method: 'POST', credentials: 'include', body: fd,
}).then(r => r.ok ? r.json() : Promise.reject(new Error('Foto-Upload fehlgeschlagen')));
},
};
// ----------------------------------------------------------

View file

@ -3,6 +3,8 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '62'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const App = (() => {
// ----------------------------------------------------------
@ -26,8 +28,7 @@ const App = (() => {
'dog-profile': { title: 'Mein Hund', module: null },
map: { title: 'Karte', module: null },
routes: { title: 'Routen', module: null },
places: { title: 'Orte', module: null },
events: { title: 'Events', module: null },
events: { title: 'Events', module: null },
poison: { title: 'Giftköder-Alarm', module: null },
walks: { title: 'Gassi-Treffen', module: null },
sitting: { title: 'Sitting', module: null },
@ -119,10 +120,12 @@ const App = (() => {
}
function _loadScript(src) {
// Versionierter URL damit SW/Browser-Cache bei jedem Deploy invalidiert wird
const versioned = `${src}?v=${APP_VER}`;
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; }
if (document.querySelector(`script[src="${versioned}"]`)) { resolve(); return; }
const s = document.createElement('script');
s.src = src;
s.src = versioned;
s.onload = resolve;
s.onerror = reject;
document.head.appendChild(s);
@ -193,11 +196,7 @@ const App = (() => {
}
function _openSidebar() {
const s = document.getElementById('sidebar');
if (!s) { UI.toast.error('sidebar: NULL'); return; }
s.classList.add('open');
const cs = getComputedStyle(s);
UI.toast.info(`display:${cs.display} | transform:${cs.transform} | z:${cs.zIndex}`);
document.getElementById('sidebar')?.classList.add('open');
document.getElementById('sidebar-backdrop')?.classList.add('visible');
}
@ -223,10 +222,7 @@ const App = (() => {
<button class="btn btn-danger w-full" data-quick="poison">
Giftköder melden
</button>
<button class="btn btn-secondary w-full" data-quick="place">
📍 Ort hinzufügen
</button>
<button class="btn btn-nature w-full" data-quick="walk">
<button class="btn btn-nature w-full" data-quick="walk">
🦮 Gassi-Treffen erstellen
</button>
</div>
@ -246,8 +242,7 @@ const App = (() => {
if (action === 'diary') { navigate('diary'); pages['diary'].module?.openNew?.(); }
if (action === 'health') { navigate('health'); pages['health'].module?.openNew?.(); }
if (action === 'poison') { navigate('poison'); pages['poison'].module?.openNew?.(); }
if (action === 'place') { navigate('places'); pages['places'].module?.openNew?.(); }
if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
if (action === 'walk') { navigate('walks'); pages['walks'].module?.openNew?.(); }
}, 350);
}, { once: true });
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff