Compare commits

...

10 commits

Author SHA1 Message Date
80b56c32ab Offline-Plan: Routen-Korridor an Routen-Feature anbinden (fehlende Verbindung)
Einstieg AUS der Route: Button 'Route offline speichern' im Routen-Detail
(_openDetail) + Navi-Ansicht, nutzt route.gps_track → Korridor = Tiles im Puffer
um den Track. Plus 'Aus meinen Routen wählen' im Offline-Dialog. War als Modus
genannt, aber ohne Einstiegspunkt/Routen-Quelle.
2026-06-05 18:03:53 +02:00
f38301a391 Offline-Plan: 'Offline-Inhalte laden' mit Bereichsauswahl für Mehrtages-Wanderungen
Zwei Modi: Aktuelle Gegend (budget-getrieben, Gassi) + Bereich auswählen (Mehrtagestour) —
Karten-Viewport/Rechteck/Routen-Korridor, Größen-Vorschau vor Download (PMTiles-Directory
aufsummieren), Liste gespeicherter Gebiete. Budget gilt im Bereichs-Modus nicht (bewusste Wahl).
2026-06-05 18:00:59 +02:00
27e7590eed Offline-Plan: Pfoten-Indikator (Welten-FAB) Segment 5 als Kopplung festgehalten
Segment 5 'Karten-Kacheln' misst aktuell OSM-Raster-Cache (offline-indicator.js,
CACHE_TILES) — GL nutzt das nicht → falsches 'Karte offline bereit'. Bei GL-Region-
Download umdefinieren (region.pmtiles + Glyphs prüfen, offline-fill-btn stößt Region-
Download an). An Offline-Strategie gekoppelt.
2026-06-05 17:55:33 +02:00
d1e44ebfb9 Offline-Plan: Referenz-Radius 10→5 km (gemessen 6,4 MB Stadt / 2,6 MB Land), Budget ~7 MB 2026-06-05 17:52:20 +02:00
daa44946f1 Docs/Karten-Plan: Wetter-Chip Niederschlag auf nächste 3h-Max umstellen
Karten-Chip zeigt aktuell Tages-Max (weather.py:98 daily.precipitation_probability_max[0]);
soll Höchstwert der nächsten 3 Std zeigen (stündliche Daten h_precip schon vorhanden:
max(h_precip[now_idx:now_idx+3])). Als abgegrenzte 'Weitere Karten-To-Dos'-Sektion festgehalten.
2026-06-05 17:48:59 +02:00
827ea95191 Tiles-Progress: Stufe 4 zeigt echte planetiler-Phase statt Müll-ETA 2026-06-05 17:21:08 +02:00
43b1e8026f Docs: Offline-Karten-Plan (GL/Vektor) — budget-getrieben + adaptives Funkloch-Lernen
Festgehalten als Feature-Plan: Region-Extract per pmtiles (budget-getrieben statt
Radius, ~16 MB Default, gemessen 15 MB für 10km München / ~18-22km Land), Client
IndexedDB-Blob + MapLibre lokal/remote, adaptives Lernen (rollendes Vorausladen
beim Aufzeichnen + Funkloch-Gedächtnis aus echten Fetch-Fehlern, alles lokal),
manuelles Vorab-Laden, Budget+LRU. Umsetzung nach Produktions-Rollout.
2026-06-05 17:16:16 +02:00
29076bcdff Tiles: Fortschritts-Snapshot-Skript (Stufe/Balken/ETA) für Build-Monitoring + .gitignore 2026-06-05 16:50:52 +02:00
d11794355c Tiles: DACH + alle Anrainer (15 Länder) + Einzel-PBFs nach Merge freigeben
TILES_REGIONS auf germany/austria/switzerland + france/italy/czech-republic/
poland/slovakia/hungary/slovenia/netherlands/belgium/luxembourg/denmark/
liechtenstein erweitert. Output bleibt dach.pmtiles (Frontend-Name stabil).
Nach time-filter werden History + Einzel-PBFs gelöscht → ~27 GB weniger
Spitzen-Plattenplatz vor planetiler.
2026-06-05 16:15:20 +02:00
c7201aa07b Karten-Attribution: standardmäßig eingeklappt (nur ⓘ) + doppelten Hinweis entfernt
Punkt 6: MapLibre rendert die Compact-Attribution offen (maplibregl-compact-show
+ open) → voller Text '© OpenStreetMap contributors' immer sichtbar. Neuer Helper
MapGLStyle.collapseAttribution() entfernt die Klasse/open nach dem Hinzufügen →
nur noch das ⓘ, der Text erscheint erst auf Klick (rechtlich nach ODbL ausreichend).
In map-gl-mini.js (Seitenkarten) + map.js (zentrale Karte) verdrahtet.

Punkt 7: poison.js + lost.js hatten UNTER der Karte zusätzlich ein hartkodiertes
'© OpenStreetMap-Mitwirkende' — doppelt zum Karten-ⓘ. Entfernt (+ ungenutzte
.lost-map-attribution CSS-Klasse). Verifiziert: osmTextLeafCount 2-3 → 1, compactShown true → false.
2026-06-05 15:48:11 +02:00
15 changed files with 226 additions and 38 deletions

2
.gitignore vendored
View file

@ -19,3 +19,5 @@ tiles/build/
*.pmtiles
*.osm.pbf
*.mbtiles
tiles/build.log
tiles/.DS_Store

View file

@ -150,7 +150,10 @@ staging-db: check-ssh
# NICHT im Image — wird per Range-Route (/tiles) ausgeliefert.
# ----------------------------------------------------------
TILES_DIR := tiles/build
TILES_REGIONS := germany austria switzerland
# DACH + alle angrenzenden Länder (15). Reihenfolge egal — osmium merge -H + time-filter
# dedupliziert Grenz-Nodes. Output bleibt dach.pmtiles (Frontend referenziert den Namen).
TILES_REGIONS := germany austria switzerland france italy czech-republic poland \
slovakia hungary slovenia netherlands belgium luxembourg denmark liechtenstein
PLANETILER_IMAGE := ghcr.io/onthegomap/planetiler:latest
TILES_TARGET := $(if $(filter prod,$(ENV)),$(DS_PATH),$(DS_PATH_STAGING))
@ -166,7 +169,8 @@ tiles:
@# liefert genau eine Version pro ID (planetiler braucht eindeutige, sortierte IDs).
@osmium merge -H $(foreach r,$(TILES_REGIONS),$(TILES_DIR)/$(r).osm.pbf) -o $(TILES_DIR)/dach-hist.osm.pbf --overwrite
@osmium time-filter $(TILES_DIR)/dach-hist.osm.pbf -o $(TILES_DIR)/dach.osm.pbf --overwrite
@rm -f $(TILES_DIR)/dach-hist.osm.pbf
@# History + Einzel-PBFs jetzt freigeben (spart ~Quellsumme an Spitzen-Plattenplatz vor planetiler).
@rm -f $(TILES_DIR)/dach-hist.osm.pbf $(foreach r,$(TILES_REGIONS),$(TILES_DIR)/$(r).osm.pbf)
@echo "→ planetiler → dach.pmtiles (disk-backed mmap)..."
@docker run --rm -v "$(CURDIR)/$(TILES_DIR):/data" $(PLANETILER_IMAGE) \
--osm-path=/data/dach.osm.pbf --download --output=/data/dach.pmtiles --force \

View file

@ -1 +1 @@
1206
1207

View file

@ -6754,15 +6754,6 @@ html.modal-open {
margin-bottom: 2px;
}
/* OSM-Attribution unter der Karte */
.lost-map-attribution {
font-size: 10px;
color: var(--c-text-secondary);
text-align: right;
padding: 2px var(--space-2) 0;
margin-bottom: var(--space-4);
}
/* Info-Zeile über der Liste ("X vermisste Hunde …") */
.lost-info-text {
font-size: var(--text-sm);

View file

@ -86,14 +86,14 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script src="/js/boot-early.js?v=1206"></script>
<script src="/js/boot-early.js?v=1207"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1206">
<link rel="stylesheet" href="/css/layout.css?v=1206">
<link rel="stylesheet" href="/css/components.css?v=1206">
<link rel="stylesheet" href="/css/utilities.css?v=1206">
<link rel="stylesheet" href="/css/lists.css?v=1206">
<link rel="stylesheet" href="/css/design-system.css?v=1207">
<link rel="stylesheet" href="/css/layout.css?v=1207">
<link rel="stylesheet" href="/css/components.css?v=1207">
<link rel="stylesheet" href="/css/utilities.css?v=1207">
<link rel="stylesheet" href="/css/lists.css?v=1207">
</head>
<body>
@ -617,11 +617,11 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1206"></script>
<script src="/js/ui.js?v=1206"></script>
<script src="/js/app.js?v=1206"></script>
<script src="/js/worlds.js?v=1206"></script>
<script src="/js/offline-indicator.js?v=1206"></script>
<script src="/js/api.js?v=1207"></script>
<script src="/js/ui.js?v=1207"></script>
<script src="/js/app.js?v=1207"></script>
<script src="/js/worlds.js?v=1207"></script>
<script src="/js/offline-indicator.js?v=1207"></script>
<!-- Feature-Seiten werden lazy geladen -->
@ -631,7 +631,7 @@
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1206"></script>
<script src="/js/boot.js?v=1207"></script>
</body>

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1206'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VER = '1207'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;

View file

@ -244,6 +244,7 @@
map.addControl(new maplibregl.AttributionControl({
compact: true, customAttribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}));
MapGLStyle.collapseAttribution(map); // nur ⓘ, nicht ausgeschrieben
// Container kann beim Erstellen (Modal/Animation) noch 0×0 sein → mehrfach resizen.
var _rz = function () { try { map.resize(); } catch (e) {} };
requestAnimationFrame(_rz);

View file

@ -141,5 +141,20 @@
};
}
window.MapGLStyle = { build: build, tilesUrl: tilesUrl, tilesFile: TILES_FILE };
// Compact-Attribution standardmäßig EINGEKLAPPT lassen (nur das ⓘ; der volle Text
// "© OpenStreetMap contributors" erscheint erst auf Klick). MapLibre rendert sie sonst
// offen (Klasse maplibregl-compact-show + open). Rechtlich reicht das ⓘ (ODbL).
function collapseAttribution(map) {
var fn = function () {
try {
var a = map.getContainer().querySelector('.maplibregl-ctrl-attrib');
if (a) { a.classList.remove('maplibregl-compact-show'); a.removeAttribute('open'); }
} catch (e) {}
};
fn();
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(fn);
setTimeout(fn, 60);
}
window.MapGLStyle = { build: build, tilesUrl: tilesUrl, tilesFile: TILES_FILE, collapseAttribution: collapseAttribution };
})();

View file

@ -107,13 +107,9 @@ window.Page_lost = (() => {
<div id="lost-map"
style="height:280px;border-radius:var(--radius-md);overflow:hidden;
margin-bottom:var(--space-4);
background:var(--c-surface-2)">
</div>
<div style="font-size:10px;color:var(--c-text-secondary);
text-align:right;margin-bottom:var(--space-4);
padding:2px var(--space-2) 0">
© OpenStreetMap-Mitwirkende
</div>
<p id="lost-info"
style="font-size:var(--text-sm);color:var(--c-text-secondary);

View file

@ -782,6 +782,7 @@ window.Page_map = (() => {
_map.addControl(new maplibregl.AttributionControl({
compact: true, customAttribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}));
MapGLStyle.collapseAttribution(_map); // nur ⓘ, nicht ausgeschrieben
if (!_userPos) {
_frankfurtTimer = setTimeout(() => _mapFlyTo(50.1109, 8.6821, 14, { duration: 2.5 }), 1200);

View file

@ -61,13 +61,9 @@ window.Page_poison = (() => {
<div id="poison-map"
style="height:280px;border-radius:var(--radius-md);overflow:hidden;
margin-bottom:var(--space-4);
background:var(--c-surface-2)">
</div>
<div style="font-size:10px;color:var(--c-text-secondary);
text-align:right;margin-bottom:var(--space-4);
padding:2px var(--space-2) 0">
© OpenStreetMap-Mitwirkende
</div>
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap;margin-bottom:var(--space-3)">
<a href="tel:110" class="btn btn-secondary" style="flex:1;text-align:center;text-decoration:none">

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="light dark">
<script src="/js/landing-init.js?v=1206"></script>
<script src="/js/landing-init.js?v=1207"></script>
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1206';
const VER = '1207';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten

122
docs/OFFLINE_MAPS_PLAN.md Normal file
View file

@ -0,0 +1,122 @@
# Offline-Karten (GL/Vektor) — Feature-Plan
**Status:** geplant (Umsetzung nach Tile-Build/Produktions-Rollout der GL-Karten).
**Stand:** 2026-06-05. Autor: René + Claude (Design).
## 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
**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)`).

60
tiles/progress.sh Executable file
View file

@ -0,0 +1,60 @@
#!/bin/bash
# Ein Fortschritts-Snapshot für den Tile-Build. Gibt EINE Zeile aus:
# STAGE=<1|2|3|4|DONE> PCT=<n> LABEL=<menschlich, mit Balken + ETA>
B=tiles/build
LOG=tiles/build.log
EXP_DL=$((24 * 1024 * 1024 * 1024)) # ~24 GB erwartete Quell-/Merge-Summe (15 Länder)
EXP_PLAN_SEC=$((90 * 60)) # ~90 Min planetiler-Schätzung (Mittel-Europa)
now=$(date +%s)
_bytes() { local s=0 f; for f in "$@"; do [ -f "$f" ] && s=$((s + $(stat -f%z "$f" 2>/dev/null || echo 0))); done; echo "$s"; }
_birth() { if [ -f "$1" ]; then stat -f%B "$1" 2>/dev/null || echo "$now"; else echo "$now"; fi; }
_mtime() { if [ -f "$1" ]; then stat -f%m "$1" 2>/dev/null || echo "$now"; else echo "$now"; fi; }
_bar() { local p=$1 n i out=""; n=$((p * 20 / 100)); [ $n -gt 20 ] && n=20; [ $n -lt 0 ] && n=0
for ((i=0;i<20;i++)); do [ $i -lt $n ] && out+="█" || out+="░"; done; printf "%s" "$out"; }
_eta() { local s=$1; [ "$s" -lt 0 ] 2>/dev/null && s=0
if [ "$s" -ge 3600 ]; then printf "~%dh %dm" $((s/3600)) $(((s%3600)/60))
elif [ "$s" -ge 60 ]; then printf "~%d Min" $((s/60)); else printf "~%ds" "$s"; fi; }
_gb() { awk "BEGIN{printf \"%.1f\", $1/1073741824}"; }
if grep -q "Tiles gebaut" "$LOG" 2>/dev/null; then
sz=$(_bytes "$B/dach.pmtiles"); echo "STAGE=DONE PCT=100 LABEL=Fertig — dach.pmtiles $(_gb $sz) GB"; exit 0
fi
if grep -q "→ planetiler" "$LOG" 2>/dev/null; then
# Echte planetiler-Zeile parsen (ANSI/\r entfernen). Phasen: osm_pass1(nodes)→osm_pass2(ways/rels)→
# write/archive(tiles). Eine ehrliche Gesamt-% gibt planetiler nicht her → Phase + Phasen-% zeigen,
# KEINE erfundene Zeit-ETA (war zuvor Quatsch wegen Log-Rauschen).
line=$(sed 's/\x1b\[[0-9;]*m//g; s/\r/\n/g' "$LOG" 2>/dev/null | grep -aE "^[0-9]+:[0-9]{2}:[0-9]{2} .*INF \[" | tail -1)
phase=$(printf '%s' "$line" | sed -nE 's/.*INF \[([a-z0-9_]+)[]:].*/\1/p')
if printf '%s' "$line" | grep -q "ways:"; then
pct=$(printf '%s' "$line" | sed -nE 's/.*ways: \[[^]]*[[:space:]]([0-9]{1,3})%[[:space:]].*/\1/p')
else
pct=$(printf '%s' "$line" | grep -oE "[0-9]{1,3}%" | tail -1 | tr -d '%')
fi
[ -z "$pct" ] && pct=0
echo "STAGE=4 PCT=$pct LABEL=planetiler · ${phase:-läuft} $(_bar $pct) ${pct}% (Phasen-Fortschritt, Gesamt-ETA unsicher)"; exit 0
fi
if [ -f "$B/dach.osm.pbf" ]; then
b=$(_bytes "$B/dach.osm.pbf"); start=$(_birth "$B/dach.osm.pbf"); el=$((now - start)); [ $el -lt 1 ] && el=1
pct=$((b * 100 / EXP_DL)); [ $pct -gt 99 ] && pct=99
rate=$((b / el)); [ $rate -lt 1 ] && rate=1; eta=$(((EXP_DL - b) / rate))
echo "STAGE=3 PCT=$pct LABEL=osmium time-filter (dedup) $(_bar $pct) ${pct}% · $(_gb $b) GB · ETA $(_eta $eta)"; exit 0
fi
if [ -f "$B/dach-hist.osm.pbf" ]; then
b=$(_bytes "$B/dach-hist.osm.pbf"); start=$(_birth "$B/dach-hist.osm.pbf"); el=$((now - start)); [ $el -lt 1 ] && el=1
pct=$((b * 100 / EXP_DL)); [ $pct -gt 99 ] && pct=99
rate=$((b / el)); [ $rate -lt 1 ] && rate=1; eta=$(((EXP_DL - b) / rate))
echo "STAGE=2 PCT=$pct LABEL=osmium merge (Grenz-Nodes) $(_bar $pct) ${pct}% · $(_gb $b) GB · ETA $(_eta $eta)"; exit 0
fi
# Stufe 1: Download
files=$(ls "$B"/*.osm.pbf 2>/dev/null | grep -v dach)
b=$(_bytes $files); start=$(_birth "$LOG"); el=$((now - start)); [ $el -lt 1 ] && el=1
pct=$((b * 100 / EXP_DL)); [ $pct -gt 99 ] && pct=99
rate=$((b / el)); [ $rate -lt 1 ] && rate=1; eta=$(((EXP_DL - b) / rate))
ndone=$(echo "$files" | grep -c .)
cur=$(grep -E "^ [a-z]" "$LOG" 2>/dev/null | tail -1 | tr -d ' ')
echo "STAGE=1 PCT=$pct LABEL=Download $(_bar $pct) ${pct}% · ${ndone}/15 Länder (${cur}) · $(_gb $b)/24 GB · ETA $(_eta $eta)"