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

@ -0,0 +1,60 @@
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
/* IE 6-8 fallback colors */
.leaflet-oldie .marker-cluster-small {
background-color: rgb(181, 226, 140);
}
.leaflet-oldie .marker-cluster-small div {
background-color: rgb(110, 204, 57);
}
.leaflet-oldie .marker-cluster-medium {
background-color: rgb(241, 211, 87);
}
.leaflet-oldie .marker-cluster-medium div {
background-color: rgb(240, 194, 12);
}
.leaflet-oldie .marker-cluster-large {
background-color: rgb(253, 156, 115);
}
.leaflet-oldie .marker-cluster-large div {
background-color: rgb(241, 128, 23);
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}

View file

@ -0,0 +1,14 @@
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
}
.leaflet-cluster-spider-leg {
/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
}

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)

View file

@ -22,8 +22,8 @@
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css">
<link rel="stylesheet" href="/css/layout.css?v=32">
<link rel="stylesheet" href="/css/components.css?v=32">
<link rel="stylesheet" href="/css/layout.css?v=62">
<link rel="stylesheet" href="/css/components.css?v=62">
</head>
<body>
@ -55,10 +55,7 @@
<div class="sidebar-item" data-page="routes">
<span class="sidebar-item-icon">🥾</span> Routen
</div>
<div class="sidebar-item" data-page="places">
<span class="sidebar-item-icon">📍</span> Orte
</div>
<div class="sidebar-item" data-page="events">
<div class="sidebar-item" data-page="events">
<span class="sidebar-item-icon">🎯</span> Events
</div>
@ -136,11 +133,7 @@
<div class="page-body page-container"></div>
</section>
<section class="page" id="page-places">
<div class="page-body page-container"></div>
</section>
<section class="page" id="page-events">
<section class="page" id="page-events">
<div class="page-body page-container"></div>
</section>
@ -214,9 +207,9 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=32"></script>
<script src="/js/ui.js?v=32"></script>
<script src="/js/app.js?v=32"></script>
<script src="/js/api.js?v=62"></script>
<script src="/js/ui.js?v=62"></script>
<script src="/js/app.js?v=62"></script>
<!-- Feature-Seiten werden lazy geladen -->

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

View file

@ -1,10 +1,11 @@
/* ============================================================
BAN YARO Service Worker
Offline-Cache + Push Notifications
Offline-Cache + Push Notifications + Tile-Cache
============================================================ */
const CACHE_VERSION = 'by-v33';
const CACHE_VERSION = 'by-v62';
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
// index.html wird NICHT pre-gecacht (immer Network-First)
const STATIC_ASSETS = [
@ -14,6 +15,9 @@ const STATIC_ASSETS = [
'/js/api.js',
'/js/ui.js',
'/js/app.js',
'/js/leaflet.markercluster.js',
'/css/MarkerCluster.css',
'/css/MarkerCluster.Default.css',
'/manifest.json',
'/icons/icon-192.png',
];
@ -30,13 +34,15 @@ self.addEventListener('install', event => {
});
// ----------------------------------------------------------
// ACTIVATE — alte Caches aufräumen
// ACTIVATE — alte Caches aufräumen (CACHE_TILES behalten)
// ----------------------------------------------------------
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys()
.then(keys => Promise.all(
keys.filter(k => k !== CACHE_STATIC).map(k => caches.delete(k))
keys
.filter(k => k !== CACHE_STATIC && k !== CACHE_TILES)
.map(k => caches.delete(k))
))
.then(() => self.clients.claim())
);
@ -59,6 +65,38 @@ self.addEventListener('fetch', event => {
return;
}
// OSM-Kartenkacheln: eigener persistenter Cache
if (url.hostname.endsWith('tile.openstreetmap.org')) {
event.respondWith(
caches.open(CACHE_TILES).then(cache =>
cache.match(event.request).then(cached => {
if (cached) return cached;
return fetch(event.request).then(response => {
if (response.ok) cache.put(event.request, response.clone());
return response;
});
})
).catch(() => new Response('', { status: 503 }))
);
return;
}
// Seiten-Module (/js/pages/…): immer Network-First (versioniert über ?v=, kein alter Cache-Treffer)
if (url.pathname.startsWith('/js/pages/')) {
event.respondWith(
fetch(event.request)
.then(response => {
if (response.ok) {
const clone = response.clone();
caches.open(CACHE_STATIC).then(c => c.put(event.request, clone));
}
return response;
})
.catch(() => caches.match(event.request))
);
return;
}
// Navigation (index.html): immer Network-First
if (event.request.mode === 'navigate') {
event.respondWith(
@ -93,6 +131,48 @@ self.addEventListener('fetch', event => {
);
});
// ----------------------------------------------------------
// MESSAGE — Tile-Vorausladung (Offline-Speicherung)
// ----------------------------------------------------------
self.addEventListener('message', event => {
if (event.data?.type !== 'CACHE_TILES') return;
const urls = event.data.urls || [];
const source = event.source;
let done = 0;
const total = urls.length;
if (total === 0) {
source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done: 0, total: 0 });
return;
}
caches.open(CACHE_TILES).then(cache => {
const queue = [...urls];
function fetchBatch() {
if (queue.length === 0) {
source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done: total, total });
return;
}
const batch = queue.splice(0, 8);
Promise.all(batch.map(url =>
cache.match(url).then(cached => {
if (cached) { done++; return; }
return fetch(url, { mode: 'cors' })
.then(r => { if (r.ok) cache.put(url, r); done++; })
.catch(() => { done++; });
})
)).then(() => {
source?.postMessage({ type: 'CACHE_TILES_PROGRESS', done, total });
setTimeout(fetchBatch, 30);
});
}
fetchBatch();
});
});
// ----------------------------------------------------------
// PUSH NOTIFICATIONS
// ----------------------------------------------------------