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

270 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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_location` → `precip = 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:98` → `precip = 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)`).