banyaro/docs/OFFLINE_MAPS_PLAN.md
rene 2cdb743ce7 Selektives Loeschen: auch Funkloch-Gebiete bleiben + Keep-Set haertung
Rene: Funkloecher + Routen waren nach 'Alles loeschen' weiter weg.
- Funkloch-Regionen jetzt im Keep-Set (geloescht wird NUR Manuelles);
  Zonen behalten ihren Fuellstatus (Komplett-Wipe setzt weiter zurueck)
- Korridor-Migration beim Loeschen: keepTracks=[{name,track}] schreibt
  Tracks in Alt-Eintraege ohne r.track (Bestand vor v1236) bzw. legt
  fehlende Korridor-Regionen an — kein Warten auf Self-Healing
- clear() liefert Summary; Toast zeigt 'behalten: Standort, X Routen,
  Y Funkloch-Gebiete' — Diagnose-Sichtbarkeit fuer Geraetetests
Bump v1237
2026-06-06 13:55:37 +02:00

20 KiB
Raw Blame History

Offline-Karten (GL/Vektor) — Feature-Plan

Status: LIVE auf Production + Staging (Default AN auf banyaro.app/.de, Prod-Freigabe René 2026-06-07 nach bestandenen Gerätetests Runde 1+2). localhost = Leaflet/AUS. Stand: 2026-06-07. Autor: René + Claude (Design).

Umsetzungsstand (2026-06-06, v1222 auf Staging)

Fertig + headless bewiesen (2026-06-05, v1213):

  • map-offline.js (window.MapOffline): Region-Download (downloadAround(lat,lon,radiusKm)) → Vektorkacheln z014 via pmtiles.getZxy (liefert bereits dekomprimierte MVT) + Glyphs in IndexedDB (by-offline-tiles). byt://-MapLibre-Protokoll (IndexedDB-first, remote-Fallback). ~15 MB / 5 km (dekomprimiert).
  • map-gl-style.js build({offline}): byt-Source statt pmtiles://.
  • ui.js/map.js laden map-offline + registrieren byt. UI.loadMapLibreUI exportiert.
  • Welten-FAB Segment 5: prüft im GL-Modus gespeicherte Region (nicht mehr OSM-Raster); „Fehlende nachladen" stößt MapOffline.downloadAround(GPS, 5km) an.
  • Beweis: Download 97 Tiles (5 km München) → Netz AUS → 1903 Features gerendert, nicht geladene Gegend (Paris) leer; Glyphs nötig (sonst lässt MapLibre offline die ganze Kachel fallen).

Follow-ups Runde 1 (2026-06-06, v1222):

  • Flag-Default Staging-AN: by_offline_tiles Default AN auf staging.banyaro.app, AUS sonst; localStorage 1/0 bzw. ?tilesoffline=1/0 (boot.js) übersteuert. Default-Logik 3× synchron: map-gl-style.js _offlineEnabled(), offline-indicator.js _offlineTilesMode(), pages/map.js _offlineTilesEnabled().
  • Karten-Download-Button: Speed-Dial „Karte offline speichern" (map-offline-btn, war seit FAB-Redesign verwaist) — GL-Modus → downloadAround(Kartenmitte, 5 km) mit Fortschritt in der Statusbar (Kartenmitte statt GPS: Urlaubsort vorab speicherbar); Leaflet-Modus → alter Raster-Prefetch (_cacheTiles). Sichtbarkeit gated: GL ohne Offline-Flag (= Production) zeigt den Button nicht.
  • Glyph-Persistenz: Glyphs in IndexedDB (Key-Präfix f/ im Tiles-Store, kein Schema-Bump) + Protokoll byt://f/{fontstack}/{range} (IndexedDB-first, remote-Fallback); Style nutzt offline die byt-Glyph-URL → überlebt App-Updates (SW-Cache wird gepurged, IndexedDB nicht).
  • Raster-Prefetch gegated: offline-indicator.js init() überspringt _prefetchTiles() im Offline-Tiles-Modus (GL nutzt das OSM-Raster nicht).

Gerätetest-Befunde behoben (2026-06-06, v1223) — Gerätetest iOS BESTANDEN (Basemap+Labels offline ok):

  • POI-Marker offline: downloadAround speichert zusätzlich /api/osm/pois (fast=true, liest lokale osm_pois-DB) je Typ für die Region-Bbox in IndexedDB (Key-Präfix p/<type>, Merge per id — zweite Region löscht die erste nicht). MapOffline.pois(type, bbox) filtert für den Ausschnitt; map.js Phase-1-Catch fällt offline darauf zurück. POI-Typen-Liste in map-offline.js synchron mit OSM_LAYER_MAP (pages/map.js) halten! Marker erscheinen erst nach ERNEUTEM Region-Download.
  • Offline-Banner klappt 5 s nach Offline-Gang auf schmale Icon-Leiste ein (volles Banner verdeckte die Karten-Legende); Banner-Styles von index.html-Inline nach components.css konsolidiert.

Runde 2 — adaptives Modell (2026-06-07, Design René 2026-06-06):

  • Budget-Download statt fester Radius: downloadAround(lat, lon, {budgetMB:5}) expandiert z14-Ringe (+ Eltern z1013, Basis z09 immer dabei) um den Standort, bis 5 MB GESPEICHERTE Bytes (dekomprimiert, IndexedDB) erreicht sind → Stadt ~1,53 km, Land ~610 km Radius — passend zur Funknetzdichte. CLIENT-seitig — der geplante Server-Region-Extract-Endpoint ist NICHT nötig.
  • Funkloch-Gedächtnis: Tile-Remote-Miss bei aktivem GPS (map.js Recording speist MapOffline.setGps) → markDeadZone (Dedupe 2 km, Cap 50, komplett lokal, nie hochgeladen). autoFillDeadZones() lädt offene Zonen budget-getrieben nach, sobald online (Trigger: offline-indicator init +30 s, online-Event +8 s; Vorab-Check ohne GL-Stack-Load).
  • Routen-Korridor: downloadCorridor(track, {bufferKm:1, capMB:50}) + Button „Offline" im Routen-Detail (rd-offline, flag-gated) — Kacheln ±1 km um den Track + Marker der Korridor-Bbox.
  • Coverage-Layer: MapOffline.coverage() (GeoJSON der gespeicherten z14-Kacheln) als blauer GL-Fill-Layer; Offline-Button öffnet jetzt ein Verwaltungs-Modal (Gebiete/MB/Marker-Stats, Gebiet speichern, Bereiche ein-/ausblenden, Alles löschen per Zweiklick).
  • Flag-Logik zentralisiert: boot.js window.BY.offlineTiles() (vorher 3× dupliziert).
  • Meta neu: regions-Liste (Cap 30) + deadzones; region (letztes Gebiet) bleibt für Back-Compat.

Gerätetest-Befunde Runde 2 behoben (v1227):

  • Giftköder + vermisste Hunde offline sichtbar (René: „müssen unbedingt sichtbar sein"): Region-Download speichert zusätzlich /api/poison + /api/lost der Gegend (p/_poison, p/_lost; Reader MapOffline.alerts(kind, bbox)). map.js _loadAll fällt pro Quelle (nicht alles-oder-nichts) auf localStorage zurück — vorher verhinderte das SW-gecachte /api/places den Fallback, während die Bbox-URL /api/poison?lat=… scheiterte. lost.js merged den Region-Snapshot in beiden Offline-Pfaden.
  • Korridor „unsichtbar": Logik war korrekt (Node-Stub-Test downloadCorridor/coverage bestanden) — er lag im bereits gespeicherten Gebiet. Nach dem Speichern werden die gespeicherten Bereiche jetzt blau auf der Routen-Detailkarte eingeblendet (_detailMap._gl).

Runde 3 (2026-06-07):

  • Offline-Indikator = pulsierendes Icon oben rechts (32 px, unterhalb der Kopfzeilen-Höhe) statt Banner über die volle Breite — verdeckte Nav-Elemente, z.B. „← Zurück" in der Routennavigation (Gerätetest René). Vollbanner weiterhin 5 s beim Offline-Gang, dann Icon.
  • Rollendes Vorausladen beim Aufzeichnen: setGps() lädt alle ~400 m still die FEHLENDEN z14±2-Kacheln (+Eltern) um die Position, solange online — deckt Weg + Anfahrt schon beim ERSTEN Besuch ab (das Funkloch-Gedächtnis greift erst ab dem 2.). Kein Region-Eintrag, kein UI.
  • Bereichsauswahl light: Modal-Button „Sichtbaren Ausschnitt speichern" → downloadBbox(viewport, {capMB:40}) mit Zu-groß-Schutz (>4000 z14-Kacheln → „bitte reinzoomen").
  • Speicher-Cap 250 MB (Soft-Guard): totalBytes-Zähler in Meta; AUTOMATISCHE Pfade (Vorausladen, Funkloch-Autofill) stoppen über dem Cap, manuelle bleiben; navigator.storage.persist() best-effort. Echte LRU-Eviction bewusst vertagt (Kacheln werden regionsübergreifend geteilt → Eviction braucht Refcounting; bei ~8 MB/Gebiet kein Druck).
  • Auto-OSM-Raster-Prefetch entfernt (offline-indicator init); _prefetchTiles/_cacheTiles bleiben nur für den manuellen Leaflet-Pfad (localhost / by_map_gl=0).
  • Logik per Node-Stub-Tests verifiziert (Bbox, Zu-groß, Cap, Prefetch-Throttle, persist). Achtung Node 21+: eingebautes navigator-Global schluckt global.navigator=-Stubs — Object.defineProperty(globalThis, 'navigator', …) verwenden.

Runde 4 — Minimal-Speicher-Modell (2026-06-08, Modell René): Prinzip: Gespeichert bleibt NUR, was wegen Funklöchern nötig ist + manuell Hinzugefügtes (jeweils mit allen Markern + Warnungen). Das Funkloch-Gedächtnis ist die QUELLE DER WAHRHEIT, die Kacheln sind jederzeit neu ableitbarer Cache.

  • Ephemeres Vorausladen: Rolling-Prefetch-Kacheln werden bei Aufzeichnungsende GELÖSCHT, wenn die Runde kein Funkloch hatte (nur neu gespeicherte Keys; Bestand unangetastet).
  • Netz-Probe bei Aufzeichnung (alle ~2 Min, /api/version, 6-s-Timeout): erkennt Funklöcher auch in BEREITS GESPEICHERTEN Gebieten — dort kommen Kacheln aus IndexedDB, es gibt keinen Remote-Miss als Signal (Lücke von René gefunden).
  • clear() behält das Funkloch-Gedächtnis (Zonen → filled:false): Beim nächsten Online-Start werden Funkloch-Gebiete automatisch neu geladen, auch wenn man alles gelöscht hat.
  • Start-Check mit Position: autoFillDeadZones({lat,lon,maxKm:50}) — nur Zonen im Umkreis (Speicher minimal), nächstgelegene zuerst; VERIFIZIERT gefüllte Zonen (Zentrums-Kachel da?) → fängt Löschen + iOS-Eviction ab. Pfote-Segment 5 = „alle bekannten Funkloch-Zonen gefüllt".
  • Coverage-Layer zweifarbig: Funkloch-Gebiete ORANGE (#f59e0b), manuelle BLAU (#3b82f6); Legende im Offline-Modal; Regionen tragen type 'funkloch'/'gebiet'/'korridor'/'ausschnitt'.

Runde 5 — Gerätetest-Feedback (2026-06-08):

  • Indikator links unter die Zoom-Regler (+/): rechts verdeckte er die Legenden-Chips.
  • Flugmodus bei offener App = Funkloch-Signal: offline-Event → Position raw als Zone in IndexedDB (GL-Stack offline evtl. nicht ladbar) → wird künftig automatisch geladen.
  • Ent-Funklochen: Zonen-Liste im Offline-Modal (Datum/Koordinaten/Status) mit ✕ → removeDeadZone(ts); Kacheln bleiben bis „Alles löschen".
  • Warnungs-Aktualität (Frage René): _mergeStore mit Bbox-Replace — die Server-Antwort ist für die geladene Bbox autoritativ, aufgehobene Giftköder/gefundene Hunde fliegen raus (Fetch-Kreis ⊇ Bbox via ×√2; fresh=null merged nie → Offline-Fetch putzt nichts weg). Zusätzlich 24-h-Refresh der Warnungen im 50-km-Umkreis beim Start-Check.
  • Routen-Korridore im Start-Check: ensureRouteCorridors(routes) — eigene Routen in Positionsnähe per Stichproben-Kacheln verifizieren, bei Lücken Korridor aus preview_track (40 Punkte, ±1-km-Puffer schluckt die Vereinfachung) neu laden. Gespeicherte Routen bleiben offline nutzbar, auch nach „Alles löschen"/Eviction. Region-Dedupe per Typ+Name.
  • Stub-Tests jetzt im Repo: tests/js/test-map-offline-r*.js (s. tests/js/README.md).

Runde 6 — Standort-Grundversorgung (2026-06-08): Renés Original-Modell Punkt 1 („die App holt sich am Standort die Karten- und Markerdaten bis 5 MB") war zu eng als nur-Funkloch interpretiert: Das Gebiet um die AKTUELLE POSITION ist jetzt IMMER geladen — ensureHomeArea(lat, lon) (Zentrums-Kachel-Check → bei Lücke Budget-Download, type 'standort', Cap-gated). Greift: (a) im Start-Check (raw IDB-Check ohne GL-Stack-Load), (b) SOFORT nach „Alles löschen" (Standort wird direkt neu geladen + Toast). Pfote Segment 5 = Standort-Kachel da UND Zonen im 50-km-Umkreis gefüllt (ferne Zonen zählen nicht mehr — sie laden erst vor Ort). Tests: tests/js/test-map-offline-r6.js.

Runde 7 — selektives Löschen (2026-06-08, Idee René: Vorladezeit sparen): „Alles löschen" löscht nicht mehr alles-und-lädt-neu, sondern behält Standort-Gebiete (type 'standort' aus der Regions-Meta), die Korridore der gespeicherten Routen (clear({keepTracks}), Tracks via API.routes.list/preview_track), den 5-km-Umkreis der aktuellen Position, Basis-Zooms 09 sowie Marker/Warnungen + Glyphs. Seit v1237 bleiben auch FUNKLOCH-Gebiete (René: sonst sofort wieder Vorladezeit) — gelöscht wird NUR Manuelles ('gebiet'/'ausschnitt'). Keep-Set ist SELBSTTRAGEND aus der Region-Meta (Korridor-Track ≤60 Pkt im Eintrag, r.track; Standort-Adoption; keepTracks=[{name,track}] migriert Alt-Einträge). clear() liefert Summary {standort, funkloch, korridore} → Toast zeigt, was behalten wurde (Diagnose). Ohne Keep-Kandidaten: Komplett-Wipe (Zonen → ungefüllt). Batch-Delete in einer Transaktion. Tests: r7 (+ r6 angepasst).

🔲 Offen (Backlog):

  • Echte LRU-Eviction (Refcounting/Region-Zuordnung der Kacheln), wenn Nutzer real ans Cap kommen.
  • Rechteck-Zeichnen als präzisere Bereichsauswahl (Viewport-Variante deckt den Hauptfall ab).
  • POIs auch beim rollenden Vorausladen (aktuell nur Kacheln; Giftköder kommen aus dem localStorage-Fallback der letzten Online-Position).

Ziel

GL-Vektorkarten offline-tauglich machen — Kernszenario Gassi/Wandern im Funkloch. Selbst-zielend (cacht wo nötig, nicht überall), speichersparsam, ohne Nutzeraufwand.

Problem (warum GL aktuell NICHT offline geht)

  • PMTiles lädt per HTTP-Range (206 Partial Content). Die Cache-API kann 206 nicht speichern (cache.put() wirft) → Basemap-Kacheln landen nie im Offline-Cache.
  • SW hat keine Regel für /tiles (nur für tile.openstreetmap.org = altes Raster).
  • offline-indicator.js prefetcht weiterhin OSM-Raster (a.tile.openstreetmap.org), das die GL-Karte gar nicht nutzt → doppelter Regress: Raster gecacht das niemand zeigt, GL-Karte offline trotzdem leer.
  • Folge offline heute: App + Daten da, aber GL-Karte = Routenlinie/Marker auf leerem Hintergrund.

Gemessene Speicher-Fakten (an echter dach.pmtiles, maxzoom=14 + Overzoom bis ~16)

Referenz-Radius = 5 km (René, 2026-06-05: „5 km genügen"). Messungen:

Gebiet Fläche Tiles Größe
München (48,1/11,5), dicht — 5 km 10×10 km 82 6,4 MB
Bayerischer Wald, ländlich — 5 km 10×10 km 99 2,6 MB
(Kontext) München 10 km 20×20 km 252 15 MB
(Kontext) Bayerischer Wald 10 km 20×20 km 285 4,4 MB
(Kontext) Bayerischer Wald ~25 km 50×50 km 1.595 20 MB

→ Vektor ist ~10× sparsamer als Raster (Raster 5 km ≈ 40120 MB). Stadt-Tiles ~2,5× dicker als Land. → 5 km ist NICHT 1/4 von 10 km (6,4 vs 15 MB) — die unteren Zoomstufen (Übersicht) sind immer dabei, unabhängig von der bbox-Größe. → Budget ≈ 7 MB (5 km dichte Stadt + Glyphs). Budget-getrieben deckt das in der Stadt ~5 km, auf dem Land ~810 km Radius ab (mehr Reichweite genau dort wo die Funklöcher sind). Glyphs (~12 MB) + Style (winzig) → ~8 MB pro Gegend. Sehr sparsam → viele Gegenden problemlos (10 ≈ 80 MB). → Messmethode (reproduzierbar): docker run --rm -v /tmp/pmt:/out protomaps/go-pmtiles:latest extract https://staging.banyaro.app/tiles/dach.pmtiles /out/x.pmtiles --bbox=W,S,E,N → Dateigröße ablesen.

Architektur

Region-Extract (budget-getrieben, NICHT fester Radius)

  • PMTiles-Directory enthält pro Tile die Byte-Länge → Server kann die Größe einer Region aufsummieren OHNE die Tiles zu laden.
  • Endpoint GET /tiles/region?lat=&lon=&budget=7 (MB): wächst die bbox um die Position, bis die summierte Tile-Länge ≈ Budget erreicht (Stadt → kleiner Radius, Land → großer Radius), extrahiert dann genau diese Region als region.pmtiles (ein 200er, ~7 MB). pmtiles extract (go-pmtiles) oder python-pmtiles im Container.
  • Client lädt die Datei einmal → IndexedDB (Blob; 200er, anders als die 206-Ranges cachebar).
  • MapLibre liest offline aus dem lokalen Blob via pmtiles:// (pmtiles.js kann aus ArrayBuffer lesen); online weiter remote dach.pmtiles (immer aktuell, ganz DACH+Anrainer). Source je nach Verbindung wählen.
  • Glyphs (/fonts/*.pbf, Open Sans Regular+Semibold) mit cachen (200er, cachebar).

Adaptive Strategie (der eigentliche Clou — lernt von selbst)

  1. Rollendes Vorausladen beim Aufzeichnen: Solange GPS aktiv UND Empfang da, fortlaufend Tiles um die aktuelle Position cachen. Deckt den echten Weg + die Anfahrt automatisch ab — auch beim ersten Mal, bevor man ins Funkloch läuft.
  2. Funkloch-Gedächtnis: Wo echte Requests scheitern (Timeout/Fehler während aktivem GPS — NICHT navigator.onLine, das lügt bei Captive-Portal/Schwachempfang), den Bereich als „Offline nötig" markieren → priorisiert behalten, beim nächsten Online-Durchgang großzügiger nachladen. Caveat: im Funkloch selbst kann nicht geladen werden → greift ab dem 2. Besuch (Gassi = repetitiv → ok).
  3. Manuelles Vorab-Laden („Offline-Inhalte laden"-Button) — zwei Modi:
    • Aktuelle Gegend (Default, Gassi): budget-getrieben um die Position (~7 MB), ein Tipp.
    • Bereich auswählen (mehrtägige Wanderung — Auto-5km reicht da nicht): Nutzer wählt ein größeres Gebiet, das ganz heruntergeladen wird. Auswahl-Optionen:
      • Karten-Ausschnitt: aktuellen Karten-Viewport (durch Zoomen/Verschieben gewählter bbox) als Download-Gebiet nehmen — simpel, kein Zeichnen nötig.
      • Rechteck ziehen auf der Karte (präziser).
      • Routen-Korridor: entlang einer Route ± Puffer (ideal für Touren, die einer Strecke folgen — viel sparsamer als eine große bbox). Verbindung zum Routen-Feature (WICHTIG, sonst hängt der Modus in der Luft): Einstieg primär AUS der Route — im Routen-Detail (routes.js _openDetail, Aktionsleiste neben GPX/Teilen/Navi) und in der Navigations-Ansicht ein Button „Route offline speichern". Nutzt den bereits geladenen route.gps_track → Korridor = alle Tiles im Puffer (z.B. 12 km) um den Track (nicht die ganze bbox). Zusätzlich im „Offline-Inhalte laden"-Dialog ein Modus „Aus meinen Routen wählen" (Liste der gespeicherten Routen → Korridor laden). Größen-Vorschau wie unten, vor dem Download.
    • Größen-Vorschau VOR dem Download: die Tile-Byte-Längen aus der PMTiles-Directory aufsummieren (kein Tile-Download nötig) → „~45 MB" anzeigen, Nutzer bestätigt. Schützt vor versehentlichem Riesen-Download.
    • Fortschritt + „X MB gespeichert" + Liste gespeicherter Gebiete (umbenennen/löschen/aktualisieren).
    • Im Bereichs-Modus gilt das ~7-MB-Budget NICHT (Nutzer entscheidet bewusst), aber eine sinnvolle Obergrenze + der globale Speicher-Cap greifen.

Drumherum

  • Budget-Cap + LRU: Gesamtspeicher gedeckelt; selten besuchte Funkloch-Caches fallen raus.
  • Privatsphäre: „Wo verliere ich Netz" = Aufenthaltsorte → komplett lokal (IndexedDB), nie hochgeladen.
  • Aktualität: Offline-Region beim nächsten Online-Sein neu ziehbar (Basemap-Updates / pmtiles-Refresh).
  • Aufräumen: Den alten OSM-Raster-Prefetch in offline-indicator.js ablösen/abschalten (cacht ungenutztes Raster).
  • Pfoten-Offline-Indikator (Welten-FAB) anpassen: offline-indicator.js füllt 5 Pfoten-Segmente; Segment 5 „Karten-Kacheln" prüft aktuell CACHE_TILES auf ≥ N OSM-Raster-Tiles (TILE_PREFETCH z14/z13). Die GL-Karte nutzt dieses Raster NICHT → grünes Segment = falsches „Karte offline bereit". → Segment 5 umdefinieren: prüfen ob für die aktuelle Gegend eine region.pmtiles + Glyphs lokal vorliegen (statt Raster-Tile-Count). offline-fill-btn („Offline-Inhalte laden") soll dann den Region-Download anstoßen statt den Raster-Prefetch. (Bei Variante 1 „Raster-Fallback" bliebe Segment 5 wie es ist — Entscheidung hängt an der Offline-Strategie oben.)

Offene Entscheidungen / Defaults

  • Budget-Default ~7 MB (Referenz 5 km Stadt; René 2026-06-05). Stadt ~5 km / Land ~810 km. Optional Stufe „Groß ~16 MB" (Stadt ~10 km / Land ~1822 km) für Wandertage.
  • Zoom z014 (Overzoom liefert Straßenebene gratis).
  • Detektionssignal = echte Fetch-Timeouts bei aktivem GPS (nicht navigator.onLine).
  • Speicher = IndexedDB (Blobs); MapLibre-Source-Umschaltung online/offline.

Abhängigkeiten

  • GL-Tiles in Produktion (dach.pmtiles + fonts auf Prod-Volume) — Voraussetzung.
  • pmtiles-Directory-Byte-Summierung (Server) + pmtiles.js Blob-Source (Client).
  • WebGL-Kontext-Disziplin beachten (siehe Skill/Memory: jede GL-Karte beim Schließen remove()).

Siehe docs/TILE_SERVER_HANDOVER.md (Tile-Pipeline) + Memory project_tile_server_maintenance.


Weitere Karten-To-Dos (nicht offline-spezifisch)

Wetter-Chip: Niederschlag „nächste 3 Std" statt ganzer Tag — ERLEDIGT (2026-06-05, v1214)

Umgesetzt: weather.py get_weather_for_locationprecip = max(h_precip[now_h:now_h+3]) (Fallback Tages-Max); Pill map.js zeigt 💧 X% (3h). Gilt auch fürs Welten-Banner (geteiltes Feld). Hinweis: forecast_days=1 → Slice am Tagesende kürzer (späte Nacht weniger Vorausschau); für volle Mitternachts-Abdeckung forecast_days=2.

(Original-Notiz:) Ist: Der Karten-Chip unten zeigt die Regenwahrscheinlichkeit als Tages-Maximum (backend/weather.py:98precip = daily['precipitation_probability_max'][0]; angezeigt in pages/map.js:~2598 als 💧 {w.precip_prob}%). Über den ganzen Tag gemittelt/maximiert = wenig aussagekräftig für „soll ich JETZT raus".

Soll: Den höchsten Wert der nächsten 3 Stunden (ab aktueller Stunde) zeigen.

  • Die stündlichen Daten werden in weather.py bereits geladen (&hourly=precipitation_probability, Array h_precip ab Zeile ~116) — kein neuer API-Call nötig.
  • Ändern: precip_prob = max(h_precip[now_idx : now_idx+3]) (aktuellen Stundenindex bestimmen wie bei der bestehenden next_rain_time-Logik). next_rain_time/Warnungen können bleiben.
  • Optional Chip-Text klarstellen, dass sich der Wert auf die nächsten 3 h bezieht (z.B. 💧 {x}% (3h)).