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

@ -1439,126 +1439,329 @@ textarea.form-control {
}
/* ============================================================
ROUTEN (routes.js)
ROUTEN Komoot-Stil (routes.js)
============================================================ */
.routes-layout {
.rk-layout {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
background: var(--c-bg);
}
.routes-tabs {
display: flex;
border-bottom: 2px solid var(--c-border-light);
flex-shrink: 0;
background: var(--c-surface);
}
.routes-tab {
flex: 1;
padding: var(--space-3) var(--space-4);
font-size: var(--text-sm);
font-weight: var(--weight-medium);
color: var(--c-text-secondary);
background: none;
border: none;
cursor: pointer;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
transition: all 0.15s;
}
.routes-tab.active {
color: var(--c-primary);
border-color: var(--c-primary);
}
.routes-tab-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.routes-map {
height: 40%;
min-height: 160px;
.rk-header {
background: var(--c-surface);
border-bottom: 1px solid var(--c-border-light);
padding: var(--space-3) var(--space-4);
flex-shrink: 0;
}
.routes-list {
.rk-search-row {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-3);
align-items: center;
}
/* Import-Label als Button */
.rk-imp-btn {
cursor: pointer;
white-space: nowrap;
flex-shrink: 0;
}
/* Import Modal */
.rk-import-preview {
height: 160px;
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--c-surface-2);
margin-bottom: var(--space-3);
}
.rk-import-stats {
display: flex;
gap: var(--space-3);
flex-wrap: wrap;
align-items: center;
font-size: var(--text-sm);
color: var(--c-text-secondary);
margin-bottom: var(--space-2);
}
.rk-search {
flex: 1;
padding: var(--space-2) var(--space-3);
border: 1.5px solid var(--c-border);
border-radius: var(--radius-full);
font-size: var(--text-sm);
background: var(--c-bg);
color: var(--c-text);
outline: none;
}
.rk-search:focus { border-color: var(--c-primary); }
.rk-rec-btn {
white-space: nowrap;
flex-shrink: 0;
}
.rk-filters {
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.rk-filter-group {
display: flex;
gap: var(--space-1);
overflow-x: auto;
scrollbar-width: none;
flex-wrap: nowrap;
}
.rk-filter-group::-webkit-scrollbar { display: none; }
.rk-chip {
padding: 4px 10px;
border-radius: var(--radius-full);
border: 1.5px solid var(--c-border);
background: var(--c-bg);
color: var(--c-text-secondary);
font-size: var(--text-xs);
white-space: nowrap;
cursor: pointer;
transition: all 0.15s;
}
.rk-chip.active {
background: var(--c-primary);
border-color: var(--c-primary);
color: #fff;
}
.rk-grid {
flex: 1;
overflow-y: auto;
padding: var(--space-3) var(--space-4);
display: flex;
flex-direction: column;
gap: var(--space-2);
gap: var(--space-3);
scrollbar-width: thin;
scrollbar-color: var(--c-primary) var(--c-surface);
}
.routes-card {
.rk-loading, .rk-empty {
text-align: center;
padding: var(--space-10) var(--space-4);
color: var(--c-text-secondary);
}
.rk-empty-icon { font-size: 3rem; margin-bottom: var(--space-3); }
.rk-empty--onboarding { padding: var(--space-6) var(--space-4); }
.rk-empty-title { font-size: var(--text-xl); font-weight: 700; color: var(--c-text-primary); margin: 0 0 var(--space-2); }
.rk-empty-text { color: var(--c-text-secondary); margin-bottom: var(--space-5); max-width: 320px; margin-left: auto; margin-right: auto; }
.rk-empty-features {
display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-2) var(--space-4);
max-width: 320px; margin: 0 auto var(--space-6); text-align: left;
}
.rk-empty-feature { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); color: var(--c-text-secondary); }
.rk-empty-feature span:first-child { font-size: 1.1rem; flex-shrink: 0; }
.rk-card {
display: flex;
align-items: flex-start;
gap: var(--space-3);
padding: var(--space-3);
flex-direction: column;
background: var(--c-surface);
border: 1.5px solid var(--c-border-light);
border-radius: var(--radius-lg);
border-radius: var(--radius-xl);
overflow: hidden;
cursor: pointer;
transition: box-shadow 0.15s;
transition: box-shadow 0.15s, transform 0.15s;
}
.routes-card:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.routes-card-num {
width: 28px;
height: 28px;
border-radius: 50%;
color: #fff;
font-weight: var(--weight-bold);
font-size: var(--text-sm);
display: flex;
align-items: center;
.rk-card:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
transform: translateY(-1px);
}
.rk-card-preview {
height: 140px;
overflow: hidden;
background: #e8f0e8;
flex-shrink: 0;
}
.rk-preview-empty {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
height: 100%;
font-size: 2.5rem;
color: var(--c-text-muted);
opacity: 0.4;
}
.routes-card-body { flex: 1; min-width: 0; }
.routes-card-name { font-weight: var(--weight-semibold); color: var(--c-text); }
.routes-card-meta { font-size: var(--text-sm); color: var(--c-text-secondary); margin-top: 2px; }
.routes-card-tags { display: flex; flex-wrap: wrap; gap: var(--space-1); margin-top: var(--space-2); }
.routes-badge {
.rk-card-body {
padding: var(--space-3) var(--space-4) var(--space-4);
}
.rk-card-name {
font-weight: var(--weight-semibold);
font-size: var(--text-base);
margin-bottom: var(--space-1);
}
.rk-card-stats {
display: flex;
gap: var(--space-3);
font-size: var(--text-sm);
color: var(--c-text-secondary);
margin-bottom: var(--space-2);
}
.rk-card-tags {
display: flex;
flex-wrap: wrap;
gap: var(--space-1);
margin-bottom: var(--space-3);
}
.rk-badge {
font-size: var(--text-xs);
padding: 2px 8px;
border-radius: var(--radius-full);
background: var(--c-surface-2);
color: var(--c-text-secondary);
}
.routes-badge--leicht { background: #dcfce7; color: #15803d; }
.routes-badge--mittel { background: #fef9c3; color: #a16207; }
.routes-badge--anspruchsvoll{ background: #fee2e2; color: #b91c1c; }
.routes-badge--info { background: var(--c-primary-subtle); color: var(--c-primary-dark); }
/* Aufzeichnungs-Panel */
.routes-rec-panel {
padding: var(--space-5) var(--space-4);
background: var(--c-surface);
flex-shrink: 0;
.rk-badge--leicht { background: #dcfce7; color: #15803d; }
.rk-badge--mittel { background: #fef9c3; color: #a16207; }
.rk-badge--anspruchsvoll{ background: #fee2e2; color: #b91c1c; }
.rk-badge--info { background: var(--c-primary-subtle); color: var(--c-primary-dark); }
.rk-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
}
.routes-rec-stats {
display: flex;
gap: var(--space-4);
margin-bottom: var(--space-4);
justify-content: center;
.rk-stars {
display: flex;
align-items: center;
gap: 2px;
}
.routes-rec-stat {
display: flex;
flex-direction: column;
align-items: center;
min-width: 70px;
}
.routes-rec-stat-val {
font-size: 1.8rem;
font-weight: var(--weight-bold);
color: var(--c-primary);
.rk-star {
font-size: 1.1rem;
cursor: pointer;
color: var(--c-border);
transition: color 0.1s, transform 0.1s;
line-height: 1;
}
.routes-rec-stat-lbl {
font-size: var(--text-xs);
color: var(--c-text-muted);
margin-top: 2px;
.rk-star.filled { color: #F59E0B; }
.rk-star:hover { color: #F59E0B; transform: scale(1.2); }
.rk-star-count { font-size: var(--text-xs); color: var(--c-text-muted); margin-left: 4px; }
.rk-card-actions {
display: flex;
align-items: center;
gap: var(--space-2);
}
.rk-card-author { font-size: var(--text-xs); color: var(--c-text-muted); }
.rk-dl-btn {
font-size: var(--text-xs);
padding: 4px 8px;
border-radius: var(--radius-md);
border: 1px solid var(--c-border);
background: var(--c-bg);
color: var(--c-text-secondary);
cursor: pointer;
white-space: nowrap;
}
.rk-dl-btn:hover { background: var(--c-surface-2); }
/* Hundetauglichkeit-Badge */
.rk-badge--dog { background: #fef3c7; color: #92400e; font-size: 1rem; }
.rk-badge--private { background: #f1f5f9; color: #64748b; }
/* Foto-Galerie in Route-Detail */
.rk-photo-gallery {
display: flex;
gap: var(--space-2);
overflow-x: auto;
margin: var(--space-2) 0;
scrollbar-width: thin;
}
.rk-photo-thumb {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: var(--radius-md);
cursor: pointer;
flex-shrink: 0;
border: 2px solid var(--c-border-light);
}
.rk-photo-add {
display: flex;
align-items: center;
justify-content: center;
width: 80px;
height: 80px;
border: 2px dashed var(--c-border);
border-radius: var(--radius-md);
cursor: pointer;
flex-shrink: 0;
font-size: 1.5rem;
color: var(--c-text-muted);
}
.rk-photo-add-empty {
display: flex;
align-items: center;
gap: var(--space-2);
padding: var(--space-3);
border: 2px dashed var(--c-border);
border-radius: var(--radius-md);
cursor: pointer;
color: var(--c-text-secondary);
font-size: var(--text-sm);
margin: var(--space-2) 0;
}
/* Nearby POIs */
.rk-nearby-section { margin-top: var(--space-3); }
.rk-nearby-title {
font-weight: var(--weight-semibold);
margin-bottom: var(--space-2);
color: var(--c-text);
}
.rk-nearby-group {
margin-bottom: var(--space-3);
}
.rk-nearby-group-label {
font-size: var(--text-sm);
font-weight: var(--weight-medium);
color: var(--c-text-secondary);
margin-bottom: var(--space-1);
}
.rk-nearby-item {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
align-items: baseline;
padding: var(--space-1) 0;
border-bottom: 1px solid var(--c-border-light);
}
.rk-nearby-item:last-child { border-bottom: none; }
.rk-nearby-name { font-size: var(--text-sm); color: var(--c-text); }
.rk-nearby-detail { font-size: var(--text-xs); color: var(--c-text-muted); }
.rk-nearby-phone { color: var(--c-primary); text-decoration: none; }
/* Hundetauglichkeit-Auswahl im Formular */
.rk-paw-select {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
margin-top: var(--space-1);
}
.rk-paw-btn {
padding: var(--space-2) var(--space-3);
border: 1.5px solid var(--c-border);
border-radius: var(--radius-full);
background: var(--c-bg);
color: var(--c-text-secondary);
font-size: var(--text-sm);
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
}
.rk-paw-btn.selected {
border-color: var(--c-primary);
background: var(--c-primary-subtle);
color: var(--c-primary-dark);
font-weight: var(--weight-medium);
}
/* Aufzeichnungs-FAB Zustand */
.map-fab--rec.recording {
background: #EF4444;
border-color: #EF4444;
color: #fff;
animation: rec-pulse 1.2s ease-in-out infinite;
}
@keyframes rec-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.5); }
50% { box-shadow: 0 0 0 8px rgba(239,68,68,0); }
}
/* ============================================================
@ -1574,50 +1777,80 @@ textarea.form-control {
z-index: 1;
}
@media (min-width: 768px) {
.map-full-layout {
top: 0;
left: var(--nav-sidebar-width);
bottom: 0;
}
}
.map-full {
width: 100%;
height: 100%;
.map-full-layout { top: 0; left: var(--nav-sidebar-width); bottom: 0; }
}
.map-full { width: 100%; height: 100%; }
/* Legende: horizontaler Scroll-Strip oben */
.map-legend {
position: absolute;
top: var(--space-3);
left: 50%;
transform: translateX(-50%);
top: var(--space-2);
left: 42px; /* Zoom-Control (+/-) freilassen */
right: 0;
z-index: 1000;
display: flex;
flex-wrap: wrap;
flex-wrap: nowrap;
gap: var(--space-1);
max-width: calc(100vw - var(--space-6));
justify-content: center;
padding: 0 var(--space-3) 0 var(--space-1);
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
pointer-events: auto;
}
.map-legend::-webkit-scrollbar { display: none; }
.map-legend-btn {
padding: var(--space-1) var(--space-2);
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 10px;
border-radius: var(--radius-full);
background: rgba(255,255,255,0.92);
background: rgba(255,255,255,0.93);
border: 1.5px solid var(--layer-color, var(--c-border));
color: var(--c-text-secondary);
font-size: var(--text-xs);
font-size: 11px;
font-weight: var(--weight-semibold);
cursor: pointer;
backdrop-filter: blur(4px);
backdrop-filter: blur(6px);
transition: all 0.15s;
box-shadow: 0 1px 4px rgba(0,0,0,0.15);
white-space: nowrap;
}
.map-legend-btn.active {
background: var(--layer-color, var(--c-primary));
color: #fff;
background: var(--layer-color, var(--c-primary));
color: #fff;
border-color: var(--layer-color, var(--c-primary));
}
.map-locate-btn {
position: absolute;
bottom: var(--space-6);
right: var(--space-4);
z-index: 1000;
.map-legend-label { font-size: 10px; }
.map-legend-all {
font-size: 1rem;
min-width: 32px;
padding: 0 var(--space-2);
background: var(--c-surface-2);
border-color: var(--c-border);
color: var(--c-text-secondary);
font-weight: var(--weight-bold);
}
.map-legend-all.all-off {
background: #1e293b;
border-color: #1e293b;
color: #fff;
}
/* FAB-Gruppe rechts unten */
.map-fabs {
position: absolute;
bottom: var(--space-4);
right: var(--space-3);
z-index: 1000;
display: flex;
flex-direction: column;
gap: var(--space-2);
align-items: center;
}
.map-fab {
width: 44px;
height: 44px;
border-radius: 50%;
@ -1625,11 +1858,247 @@ textarea.form-control {
border: 1.5px solid var(--c-border);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
cursor: pointer;
font-size: 1.3rem;
font-size: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, border-color 0.15s;
-webkit-tap-highlight-color: transparent;
}
.map-fab:hover { background: var(--c-surface-2); }
.map-fab--pin.active {
background: var(--c-danger);
border-color: var(--c-danger);
color: #fff;
font-size: 1rem;
}
.map-fab:disabled { opacity: 0.5; cursor: default; }
.map-fab--offline.loading { animation: fab-spin 1.2s linear infinite; pointer-events: none; }
@keyframes fab-spin { to { transform: rotate(360deg); } }
/* Aufzeichnungs-Panel — schiebt sich von unten rein */
.map-rec-panel {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(8px);
color: #fff;
padding: var(--space-4) var(--space-5) calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
transform: translateY(100%);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 420;
pointer-events: none;
}
.map-rec-panel.active {
transform: translateY(0);
pointer-events: all;
}
.map-rec-stats {
display: flex;
justify-content: space-around;
margin-bottom: var(--space-4);
}
.map-rec-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.map-rec-stat--main .map-rec-val { font-size: 2.4rem; }
.map-rec-val {
font-size: 1.8rem;
font-weight: 800;
line-height: 1;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
.map-rec-lbl {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: rgba(255,255,255,0.55);
}
.map-rec-actions {
display: flex;
gap: var(--space-3);
margin-bottom: var(--space-3);
}
.map-rec-action-btn { flex: 1; }
.map-rec-hint {
text-align: center;
font-size: var(--text-xs);
color: rgba(255,255,255,0.45);
}
.map-rec-panel.paused .map-rec-val { color: #F59E0B; }
.map-rec-panel.paused .map-rec-hint::before { content: '⏸ Pausiert — '; }
/* Fadenkreuz-Overlay */
.map-crosshair {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -100%);
z-index: 410;
pointer-events: none;
display: none;
flex-direction: column;
align-items: center;
}
.map-crosshair.active { display: flex; }
.map-crosshair-pin {
font-size: 2.4rem;
line-height: 1;
filter: drop-shadow(0 3px 6px rgba(0,0,0,0.45));
transition: transform 0.15s cubic-bezier(0.34,1.56,0.64,1);
}
.map-crosshair.dragging .map-crosshair-pin {
transform: translateY(-10px) scale(1.15);
}
.map-crosshair-shadow {
width: 10px;
height: 4px;
background: rgba(0,0,0,0.25);
border-radius: 50%;
margin-top: 1px;
transition: all 0.15s;
}
.map-crosshair.dragging .map-crosshair-shadow {
width: 6px; opacity: 0.4;
}
/* Bestätigen-Leiste unten */
.map-place-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: var(--c-surface);
border-top: 1px solid var(--c-border-light);
padding: var(--space-3) var(--space-4) calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
display: none;
flex-direction: column;
gap: var(--space-2);
z-index: 410;
box-shadow: 0 -4px 20px rgba(0,0,0,0.08);
}
.map-place-bar.active { display: flex; }
.map-place-hint {
text-align: center;
font-size: var(--text-sm);
color: var(--c-text-secondary);
}
.map-place-btns {
display: flex;
gap: var(--space-3);
}
.map-place-btns .btn { flex: 1; }
/* Statusleiste: nur Info, unten links */
.map-statusbar {
position: absolute;
bottom: var(--space-3);
left: var(--space-3);
z-index: 1000;
display: flex;
align-items: center;
gap: var(--space-2);
background: rgba(255,255,255,0.88);
backdrop-filter: blur(4px);
border: 1px solid var(--c-border-light);
border-radius: var(--radius-full);
padding: 4px 12px;
font-size: 11px;
color: var(--c-text-secondary);
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
max-width: calc(100% - 80px); /* Platz für FABs */
pointer-events: none;
}
/* Giftköder-Marker — pulsierend, rot, sofort erkennbar */
.poison-marker {
position: relative;
width: 48px; height: 48px;
}
.poison-dot {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: 44px; height: 44px;
background: #DC2626;
border: 3px solid #fff;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 20px;
box-shadow: 0 2px 10px rgba(220,38,38,0.7);
z-index: 2;
}
.poison-ring {
position: absolute;
top: 50%; left: 50%;
width: 44px; height: 44px;
border-radius: 50%;
background: rgba(220,38,38,0.35);
animation: poison-pulse 1.8s ease-out infinite;
z-index: 1;
}
.poison-ring:nth-child(2) { animation-delay: 0.6s; }
@keyframes poison-pulse {
0% { transform: translate(-50%,-50%) scale(1); opacity: 0.8; }
100% { transform: translate(-50%,-50%) scale(2.8); opacity: 0; }
}
/* Pulsierender Standort-Marker */
.loc-icon { position: relative; }
.loc-dot {
width: 16px; height: 16px;
background: #3B82F6;
border: 3px solid #fff;
border-radius: 50%;
box-shadow: 0 1px 4px rgba(0,0,0,0.3);
position: absolute;
top: 4px; left: 4px;
}
.loc-ring {
width: 24px; height: 24px;
background: rgba(59,130,246,0.3);
border-radius: 50%;
position: absolute;
top: 0; left: 0;
animation: loc-pulse 2s ease-out infinite;
}
@keyframes loc-pulse {
0% { transform: scale(0.8); opacity: 1; }
100% { transform: scale(2.2); opacity: 0; }
}
/* Pin-Typ-Auswahl im Modal */
.poi-type-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--space-2);
}
.poi-type-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px 4px;
border: 2px solid var(--c-border);
border-radius: var(--radius-md);
background: var(--c-surface);
cursor: pointer;
transition: border-color 0.15s, background 0.15s;
-webkit-tap-highlight-color: transparent;
}
.poi-type-btn.selected {
border-color: var(--pt-color, var(--c-primary));
background: color-mix(in srgb, var(--pt-color, var(--c-primary)) 12%, transparent);
}
.poi-type-icon { font-size: 22px; line-height: 1; }
.poi-type-label { font-size: 10px; color: var(--c-text-secondary); text-align: center; line-height: 1.2; }
/* ------------------------------------------------------------
GASSI-TREFFEN (walks.js)