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:
parent
bf26e5faf4
commit
ebe4ce20cf
16 changed files with 3020 additions and 737 deletions
|
|
@ -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')));
|
||||
},
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
2
backend/static/js/leaflet.markercluster.js
Normal file
2
backend/static/js/leaflet.markercluster.js
Normal file
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
Loading…
Add table
Add a link
Reference in a new issue