diff --git a/VERSION b/VERSION index 1fdcf1c..ededf28 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1228 \ No newline at end of file +1220 \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index e399801..bf4f74b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2601,18 +2601,6 @@ async def wurfboerse_page(): return HTMLResponse(content=html, headers={"Cache-Control": "max-age=1800"}) -# Rechtsseiten: Pfad-URLs (SEO-Footer, App-Store-Metadaten, E-Mails) auf die -# SPA-Hash-Routen umleiten — die Inhalte leben als SPA-Seiten (#agb, …). -# Muss VOR dem SPA-Fallback registriert sein. -@app.get("/agb") -@app.get("/datenschutz") -@app.get("/impressum") -async def legal_page_redirect(request: _Request): - from fastapi.responses import RedirectResponse - page = request.url.path.strip("/") - return RedirectResponse(f"/#{page}", status_code=302) - - # SPA Fallback — ALLE nicht-API-Routen gehen zur index.html @app.get("/{full_path:path}") async def spa_fallback(full_path: str): diff --git a/backend/static/css/components.css b/backend/static/css/components.css index 5b2a7dd..87dea9c 100644 --- a/backend/static/css/components.css +++ b/backend/static/css/components.css @@ -7294,25 +7294,14 @@ svg.empty-state-icon { left: 0; right: 0; z-index: 9999; - background: #1f2937; - color: #f3f4f6; - font-size: 0.78rem; - font-weight: 500; + background: var(--c-text-secondary, #6b7280); + color: #fff; + font-size: var(--text-sm); text-align: center; - padding: calc(env(safe-area-inset-top, 0px) + 7px) 16px 7px; - align-items: center; - justify-content: center; - gap: 8px; - box-shadow: 0 2px 8px rgba(0,0,0,.3); + padding: var(--space-2) var(--space-4); pointer-events: none; letter-spacing: 0.01em; } -/* Eingeklappt (5s nach Offline-Gang, boot.js): schmale Icon-Leiste statt 2-Zeilen-Banner — - das volle Banner verdeckte die Karten-Steuerung oben (Gerätetest iOS 2026-06-06). */ -#offline-banner.collapsed { - padding: calc(env(safe-area-inset-top, 0px) + 2px) 16px 2px; -} -#offline-banner.collapsed #offline-banner-text { display: none; } /* ------------------------------------------------------------ STREAK-WIDGET (Welcome-Seite) diff --git a/backend/static/index.html b/backend/static/index.html index ab2e972..9b1271c 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,19 +86,24 @@ Ban Yaro - + - - - - - + + + + + - -
+ + - ${(!_useGL() || _offlineTilesEnabled()) ? ` -
- Karte offline speichern - -
` : ''} ${App.hasPro(_appState?.user) ? `
Regenradar @@ -373,11 +368,6 @@ window.Page_map = (() => { _sdEl?.classList.remove('open'); _togglePlacementMode(); }); - document.getElementById('map-offline-btn')?.addEventListener('click', () => { - _sdEl?.classList.remove('open'); - if (_engineGL) _openOfflineModal(); // GL: Verwaltung (speichern/anzeigen/löschen) - else _cacheTiles(); // Leaflet: OSM-Raster → SW-Cache - }); document.getElementById('map-radar-btn')?.addEventListener('click', () => { _sdEl?.classList.remove('open'); _toggleRadar(); @@ -773,12 +763,6 @@ window.Page_map = (() => { } catch (e) { return false; } } - // Offline-Vektorkacheln-Flag — zentrale Logik in boot.js BY.offlineTiles(). - // Steuert nur die Button-Sichtbarkeit: im GL-Modus ohne byt://-Quelle wäre der Download nutzlos. - function _offlineTilesEnabled() { - try { return !!(window.BY && BY.offlineTiles()); } catch (e) { return false; } - } - function loadMapLibre() { if (_maplibreLoaded) return Promise.resolve(); const v = '?v=' + (window.APP_VER || ''); @@ -879,7 +863,6 @@ window.Page_map = (() => { const el = document.getElementById('central-map'); if (!el || !window.maplibregl || _map) return; _engineGL = true; - _covOn = false; // Bereiche-Layer-Status gehört zur Karten-Instanz const center = _userPos ? [_userPos.lon, _userPos.lat] : [8.6821, 50.1109]; // Frankfurt [lng,lat] const zoom = _userPos ? 14 : 10; @@ -1464,33 +1447,15 @@ window.Page_map = (() => { } } - // POIs holen — WICHTIG: r.ok prüfen! Der SW antwortet offline auf nicht-cachebare - // API-GETs mit 503 + JSON-Body ({detail:…}) → r.json() wirft NICHT, der Erfolgs-Pfad - // liefe mit einem Objekt statt Array weiter und ersetzte die Marker durch nichts. - const _fetchPois = async (params) => { - const r = await fetch(`/api/osm/pois?${params}`); - if (!r.ok) throw new Error(`pois ${r.status}`); - const pois = await r.json(); - return Array.isArray(pois) ? pois : []; - }; - // Phase 1: sofort DB-Daten zeigen (fast=true) _setOsmStatus('Lade…'); const fastTasks = activeLayers.map(async ([layerKey, osmType]) => { const params = new URLSearchParams({ type: osmType, fast: 'true', ...bbox }); try { - const pois = await _fetchPois(params); + const pois = await fetch(`/api/osm/pois?${params}`).then(r => r.json()); _replaceOsmMarkers(layerKey, pois); return pois.length; - } catch { - // Offline: gespeicherte Region-POIs aus IndexedDB (MapOffline.downloadAround - // legt sie beim Region-Download mit ab) statt leerer Karte. - try { - const off = window.MapOffline ? await MapOffline.pois(osmType, bbox) : []; - if (off.length) { _replaceOsmMarkers(layerKey, off); return off.length; } - } catch (e) {} - return 0; - } + } catch { return 0; } }); const fastCounts = await Promise.all(fastTasks); const fastTotal = fastCounts.reduce((a, b) => a + b, 0); @@ -1504,7 +1469,7 @@ window.Page_map = (() => { const freshTasks = activeLayers.map(async ([layerKey, osmType]) => { const params = new URLSearchParams({ type: osmType, ...bbox }); try { - const pois = await _fetchPois(params); + const pois = await fetch(`/api/osm/pois?${params}`).then(r => r.json()); const osmCount = _osmCountOf(layerKey); if (pois.length !== osmCount) _replaceOsmMarkers(layerKey, pois); _done++; @@ -1860,9 +1825,6 @@ window.Page_map = (() => { credentials: 'include', body: JSON.stringify(body), }); if (res.status === 401) { UI.toast.error('Bitte erst anmelden.'); return; } - // res.ok prüfen: SW antwortet offline mit 503+JSON → json() wirft nicht, - // sonst Erfolgs-Toast obwohl nichts gemeldet wurde. (202 = offline gequeued = ok.) - if (!res.ok) throw new Error(`report ${res.status}`); const data = await res.json(); if (data.status === 'bereits_gemeldet') { UI.toast.info('Du hast diesen Marker bereits gemeldet.'); @@ -1918,40 +1880,29 @@ window.Page_map = (() => { API.breeder.mapMarkers(), ]); - // Offline-Fallback PRO QUELLE (nicht alles-oder-nichts): Der SW cached /api/places und - // /api/breeder/map-markers (feste URLs), aber /api/poison?lat=… ändert sich mit jeder - // Position → Cache-Miss → vorher verschwanden offline ausgerechnet die GIFTKÖDER, - // während places aus dem SW-Cache kam und den allFailed-Fallback verhinderte - // (Gerätetest 2026-06-07). Jede Quelle fällt einzeln auf den letzten guten Stand zurück. - let cached = null; - try { cached = JSON.parse(localStorage.getItem(_MAP_POI_KEY) || 'null'); } catch {} const allFailed = [places, poisonList, breederList].every(r => r.status === 'rejected'); - - const placesVal = places.status === 'fulfilled' ? places.value : (cached?.places || []); - let poisonVal = poisonList.status === 'fulfilled' ? poisonList.value : (cached?.poison || []); - const breederVal = breederList.status === 'fulfilled' ? breederList.value : (cached?.breeders || []); - - // Giftköder zusätzlich aus dem Offline-Region-Snapshot (deckt vorab gespeicherte - // Gegenden ab, wo der localStorage-Stand der letzten Position nicht hinreicht). - if (poisonList.status === 'rejected' && window.MapOffline?.alerts) { + if (allFailed) { try { - const c = _map ? _map.getCenter() : (_userPos ? { lat: _userPos.lat, lng: _userPos.lon } : null); - if (c) { - const off = await MapOffline.alerts('poison', - { south: c.lat - 0.5, north: c.lat + 0.5, west: c.lng - 0.7, east: c.lng + 0.7 }); - const seen = new Set(poisonVal.map(p => p.id)); - poisonVal = poisonVal.concat(off.filter(p => !seen.has(p.id))); + const raw = localStorage.getItem(_MAP_POI_KEY); + if (raw) { + const cached = JSON.parse(raw); + _addPlaces(cached.places || []); + _addPoison(cached.poison || []); + _addBreeders(cached.breeders || []); + UI.toast.info('Offline — Karte zeigt gecachte Kacheln. POI-Daten eventuell veraltet.'); + _scheduleOsmLoad(); + return; } } catch {} } - if (allFailed && (placesVal.length || poisonVal.length || breederVal.length)) { - UI.toast.info('Offline — Karte zeigt zuletzt geladene Daten.'); - } + const placesVal = places.status === 'fulfilled' ? places.value : []; + const poisonVal = poisonList.status === 'fulfilled' ? poisonList.value : []; + const breederVal = breederList.status === 'fulfilled' ? breederList.value : []; - _addPlaces(placesVal); - _addPoison(poisonVal); - _addBreeders(breederVal); + if (places.status === 'fulfilled') _addPlaces(placesVal); + if (poisonList.status === 'fulfilled') _addPoison(poisonVal); + if (breederList.status === 'fulfilled') _addBreeders(breederVal); if (places.status === 'fulfilled' || poisonList.status === 'fulfilled' || breederList.status === 'fulfilled') { try { @@ -2164,103 +2115,6 @@ window.Page_map = (() => { return urls; } - // GL-Modus: Gebiet um die KARTENMITTE budget-getrieben (~5 MB) speichern — Stadt klein, - // Land groß (Ring-Wachstum in MapOffline). Kartenmitte statt GPS, damit man eine entfernte - // Gegend (Urlaubsort) vorab speichern kann. docs/OFFLINE_MAPS_PLAN.md - async function _downloadVectorRegion() { - if (!_map || !window.MapOffline) return; - const btn = document.getElementById('map-offline-btn'); - if (btn?.classList.contains('loading')) return; // läuft bereits - const c = _map.getCenter(); - btn?.classList.add('loading'); - _setOsmStatus('Offline: 0 MB…'); - try { - const res = await MapOffline.downloadAround(c.lat, c.lng, { budgetMB: 5, onProgress: p => { - _setOsmStatus(`Offline: ${(p.bytes / 1048576).toFixed(1)} / ${Math.round(p.budget / 1048576)} MB…`); - } }); - _setOsmStatus(''); - UI.toast.success(`Gegend offline gespeichert — ~${res.radiusKm} km Umkreis, ${res.pois || 0} Marker, ${(res.bytes / 1048576).toFixed(1)} MB.`); - window.OfflineIndicator?.refresh(); // Pfoten-Segment 5 sofort grün - if (_covOn) _setCoverage(true); // Bereiche-Layer aktualisieren - } catch (e) { - _setOsmStatus(''); - UI.toast.error('Offline-Download fehlgeschlagen — bitte erneut versuchen.'); - } finally { - btn?.classList.remove('loading'); - } - } - - // ---------------------------------------------------------- - // Offline-Bereiche-Layer (gespeicherte z14-Kacheln) + Verwaltungs-Modal - // ---------------------------------------------------------- - let _covOn = false; - async function _setCoverage(on) { - if (!_engineGL || !_map || !window.MapOffline) return false; - if (!on) { - try { - if (_map.getLayer('by-off-cov-line')) _map.removeLayer('by-off-cov-line'); - if (_map.getLayer('by-off-cov')) _map.removeLayer('by-off-cov'); - if (_map.getSource('by-off-cov')) _map.removeSource('by-off-cov'); - } catch (e) {} - _covOn = false; - return false; - } - const gj = await MapOffline.coverage().catch(() => null); - if (!gj || !gj.features.length) { UI.toast.info('Noch keine Offline-Bereiche gespeichert.'); return false; } - if (_map.getSource('by-off-cov')) { - _map.getSource('by-off-cov').setData(gj); - } else { - _map.addSource('by-off-cov', { type: 'geojson', data: gj }); - _map.addLayer({ id: 'by-off-cov', type: 'fill', source: 'by-off-cov', - paint: { 'fill-color': '#3b82f6', 'fill-opacity': 0.15 } }); - _map.addLayer({ id: 'by-off-cov-line', type: 'line', source: 'by-off-cov', - paint: { 'line-color': '#3b82f6', 'line-opacity': 0.35, 'line-width': 0.5 } }); - } - _covOn = true; - return true; - } - - // Verwaltungs-Modal am Offline-Button: Stats + Gebiet speichern / Bereiche anzeigen / Löschen. - async function _openOfflineModal() { - if (!window.MapOffline) return; - let s = { regions: [] }; - try { s = await MapOffline.stats(); } catch (e) {} - const regions = s.regions || []; - const totalBytes = regions.reduce((a, r) => a + (r.bytes || 0), 0); - const totalPois = regions.reduce((a, r) => a + (r.pois || 0), 0); - UI.modal.open({ - title: '🗺️ Offline-Karten', - body: ` -

- ${regions.length - ? `${regions.length} ${regions.length === 1 ? 'Gebiet' : 'Gebiete'} gespeichert — ~${(totalBytes / 1048576).toFixed(1)} MB, ${totalPois} Marker.` - : 'Noch kein Gebiet gespeichert. Karte und Marker bleiben damit auch im Funkloch verfügbar.'} -

-
- - - ${regions.length ? `` : ''} -
- `, - footer: ``, - }); - document.getElementById('off-dl')?.addEventListener('click', () => { UI.modal.close(); _downloadVectorRegion(); }); - document.getElementById('off-cov')?.addEventListener('click', async () => { UI.modal.close(); await _setCoverage(!_covOn); }); - document.getElementById('off-clear')?.addEventListener('click', async e => { - const btn = e.currentTarget; - if (btn.dataset.confirm !== '1') { // Zweiklick statt confirm-Modal im Modal - btn.dataset.confirm = '1'; - btn.innerHTML = `${UI.icon('trash')} Wirklich alles löschen?`; - return; - } - await MapOffline.clear().catch(() => {}); - _setCoverage(false); - UI.modal.close(); - UI.toast.success('Offline-Karten gelöscht.'); - window.OfflineIndicator?.refresh(); - }); - } - async function _cacheTiles() { if (!_map) return; if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) { @@ -2489,9 +2343,6 @@ window.Page_map = (() => { _recDistKm += d / 1000; } _recTrack.push({ lat, lon }); - // Funkloch-Gedächtnis: Position melden — Tile-Fetch-Fehler bei aktivem GPS - // markieren die Gegend als „Offline nötig" (lokal, map-offline.js). - window.MapOffline?.setGps({ lat, lon }); _persistRec(); _updateRecMap(lat, lon); _updateRecStatus(); @@ -2638,7 +2489,6 @@ window.Page_map = (() => { if (_recWatchId !== null) { navigator.geolocation.clearWatch(_recWatchId); _recWatchId = null; } if (_recTimerInt) { clearInterval(_recTimerInt); _recTimerInt = null; } _recActive = false; - window.MapOffline?.setGps(null); // Funkloch-Erkennung nur bei aktiver Aufzeichnung _releaseWakeLock(); _hidePocketOverlay(); document.removeEventListener('visibilitychange', _onVisibilityChange); diff --git a/backend/static/js/pages/routes.js b/backend/static/js/pages/routes.js index 68729c7..23e3381 100644 --- a/backend/static/js/pages/routes.js +++ b/backend/static/js/pages/routes.js @@ -2437,7 +2437,6 @@ window.Page_routes = (() => { ${_actionBtn('rd-share', 'arrow-square-out', 'Teilen')} ${_actionBtn('rd-navi', 'map-pin', 'Navi')} ${_appState.user ? _actionBtn('rd-note', 'note-pencil', 'Notiz') : ''} - ${(window.BY?.offlineTiles?.() && track.length >= 2) ? _actionBtn('rd-offline', 'cloud-arrow-down', 'Offline') : ''}
${ownerRow} @@ -2461,45 +2460,6 @@ window.Page_routes = (() => { document.getElementById('rd-close')?.addEventListener('click', UI.modal.close); document.getElementById('rd-gpx')?.addEventListener('click', () => _downloadGpxDirect(route)); - // Route offline speichern: Kachel-Korridor ±1 km um den Track + Marker → IndexedDB - // (für mehrtägige Unternehmungen entlang der Route, docs/OFFLINE_MAPS_PLAN.md). - document.getElementById('rd-offline')?.addEventListener('click', async () => { - const btn = document.getElementById('rd-offline'); - if (!btn || btn.dataset.busy) return; - btn.dataset.busy = '1'; - const label = btn.querySelector('span'); - try { - await UI.loadMapLibreUI(); // lädt pmtiles + map-offline (byt://-Stack) bei Bedarf - const res = await MapOffline.downloadCorridor(track, { - bufferKm: 1, name: route.name, - onProgress: p => { if (label) label.textContent = `${(p.bytes / 1048576).toFixed(1)} MB`; }, - }); - if (label) label.textContent = 'Offline ✓'; - UI.toast.success(`Route offline gespeichert — Korridor ±1 km, ${res.pois || 0} Marker, ` - + `${(res.bytes / 1048576).toFixed(1)} MB.${res.capped ? ' (50-MB-Limit erreicht)' : ''}`); - window.OfflineIndicator?.refresh(); - // Gespeicherte Bereiche sofort auf der Detailkarte zeigen (blau) — sonst ist der - // Korridor „unsichtbar", v.a. wenn er im schon gespeicherten Gebiet liegt. - try { - const gl = _detailMap?._gl; - if (gl) { - const gj = await MapOffline.coverage(); - if (gl.getSource('rd-off-cov')) gl.getSource('rd-off-cov').setData(gj); - else { - gl.addSource('rd-off-cov', { type: 'geojson', data: gj }); - gl.addLayer({ id: 'rd-off-cov', type: 'fill', source: 'rd-off-cov', - paint: { 'fill-color': '#3b82f6', 'fill-opacity': 0.15 } }); - } - } - } catch (e) {} - } catch (e) { - if (label) label.textContent = 'Offline'; - UI.toast.error('Offline-Speichern fehlgeschlagen.'); - } finally { - delete btn.dataset.busy; - } - }); - // Teilen-Button document.getElementById('rd-share')?.addEventListener('click', async () => { const shareUrl = location.origin + '/#routes?id=' + route.id; @@ -2802,11 +2762,8 @@ window.Page_routes = (() => { await Promise.all(NEARBY_TYPES.map(async ({ type, icon, label, svgIcon, color }) => { try { const params = new URLSearchParams({ type, fast: 'true', ...bbox }); - // r.ok prüfen: SW antwortet offline mit 503+JSON ({detail:…}) → json() wirft nicht - const r = await fetch(`/api/osm/pois?${params}`); - if (!r.ok) throw new Error(`pois ${r.status}`); - const pois = await r.json(); - (Array.isArray(pois) ? pois : []) + const pois = await fetch(`/api/osm/pois?${params}`).then(r => r.json()); + pois .filter(p => _isNearTrack(p, track, 100)) // max 100m vom Track-Verlauf .forEach(p => results.push({ ...p, _icon: icon, _label: label, _svgIcon: svgIcon, _color: color })); } catch {} diff --git a/backend/static/js/pages/settings.js b/backend/static/js/pages/settings.js index a9446bb..e59aaeb 100644 --- a/backend/static/js/pages/settings.js +++ b/backend/static/js/pages/settings.js @@ -1346,9 +1346,6 @@ window.Page_settings = (() => { try { // Versionsnummer direkt vom API-Endpunkt holen const r = await fetch('/api/version', { cache: 'no-store' }); - // r.ok prüfen: SW antwortet offline mit 503+JSON → json() wirft nicht und - // serverVersion=undefined meldete fälschlich „Ban Yaro ist aktuell". - if (!r.ok) throw new Error(`version ${r.status}`); const { version: serverVersion } = await r.json(); const localVersion = typeof APP_VER !== 'undefined' ? APP_VER : '0'; diff --git a/backend/static/js/pages/social.js b/backend/static/js/pages/social.js index b87cf8d..4a81765 100644 --- a/backend/static/js/pages/social.js +++ b/backend/static/js/pages/social.js @@ -377,12 +377,7 @@ window.Page_social = (() => { method: 'POST', headers: {Authorization: `Bearer ${localStorage.getItem('by_token')}`}, body: fd, - }).then(r => { - // r.ok prüfen: SW antwortet offline mit 503+JSON → json() wirft nicht, d.url wäre undefined - if (!r.ok) throw new Error(`upload ${r.status}`); - return r.json(); - }).then(d => { uploadedMediaUrl = d.url; }) - .catch(() => UI.toast.error('Medien-Upload fehlgeschlagen.')); + }).then(r => r.json()).then(d => { uploadedMediaUrl = d.url; }); }; reader.readAsDataURL(file); } diff --git a/backend/static/landing.html b/backend/static/landing.html index c662fdf..ddc5d3d 100644 --- a/backend/static/landing.html +++ b/backend/static/landing.html @@ -4,7 +4,7 @@ - + Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz diff --git a/backend/static/sw.js b/backend/static/sw.js index c0390b0..c168f51 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -4,7 +4,7 @@ ============================================================ */ // ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab -const VER = '1228'; +const VER = '1220'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten diff --git a/docker-compose.osm.yml b/docker-compose.osm.yml index e496720..c786098 100644 --- a/docker-compose.osm.yml +++ b/docker-compose.osm.yml @@ -12,8 +12,5 @@ services: - ./data:/data # gleiche DB wie die App (/data/banyaro.db) environment: - DB_PATH=/data/banyaro.db - # Abdeckung = TILES_REGIONS im Makefile — Karten- und POI-Abdeckung - # synchron halten! Env überschreibt den Default in refresh.sh, daher - # wirkt eine Änderung hier OHNE Image-Rebuild. - - COUNTRIES=germany austria switzerland france italy czech-republic poland slovakia hungary slovenia netherlands belgium luxembourg denmark liechtenstein + # - COUNTRIES=switzerland austria germany # bei Bedarf überschreiben restart: "no" diff --git a/docs/OFFLINE_MAPS_PLAN.md b/docs/OFFLINE_MAPS_PLAN.md index c440341..73135e3 100644 --- a/docs/OFFLINE_MAPS_PLAN.md +++ b/docs/OFFLINE_MAPS_PLAN.md @@ -1,83 +1,28 @@ # 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). +**Status:** KERN UMGESETZT + headless verifiziert (2026-06-05, v1213), **flag-gated `by_offline_tiles` (Default AUS)** bis Gerätetest. +**Stand:** 2026-06-05. Autor: René + Claude (Design). -## Umsetzungsstand (2026-06-06, v1222 auf Staging) -**✅ Fertig + headless bewiesen (2026-06-05, v1213):** +## Umsetzungsstand (2026-06-05) +**✅ Fertig + headless bewiesen:** - `map-offline.js` (`window.MapOffline`): Region-Download (`downloadAround(lat,lon,radiusKm)`) → Vektorkacheln z0–14 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://`. +- `map-gl-style.js` `build({offline})`: `byt`-Source statt `pmtiles://`. Flag `by_offline_tiles` (Default AUS). - 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/`, 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 z10–13, Basis z0–9 immer dabei) um den Standort, bis **5 MB GESPEICHERTE Bytes** - (dekomprimiert, IndexedDB) erreicht sind → Stadt ~1,5–3 km, Land ~6–10 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`). - -**🔲 Offen (Runde 3):** -- **Gerätetest Runde 2** (Budget-Download, Funkloch-Lernen auf echter Gassi-Runde, Korridor, - Coverage-Layer) → dann Prod-Freigabe-Entscheidung (BY.offlineTiles-Default erweitern analog `by_map_gl`). -- **Rollendes Vorausladen beim Aufzeichnen** (fortlaufend um die aktuelle Position cachen, solange - Empfang da — deckt den Weg schon beim ersten Mal ab; Akku-/Datensparsamkeit beachten). -- **Bereichsauswahl** (Karten-Ausschnitt/Rechteck als Download-Gebiet) — Korridor deckt den - Hauptfall ab, Rest nach Bedarf. -- **Speicher-Cap + LRU** über alles (alte Gebiete fliegen automatisch raus) + optional - `navigator.storage.persist()`. -- Alten OSM-Raster-Prefetch (`offline-indicator.js _prefetchTiles` + `map.js _cacheTiles`) komplett - entfernen, wenn Flag dauerhaft AN (auch Prod). +**🔲 Offen (Follow-ups):** +- **Gerätetest (iOS-PWA offline/IndexedDB)** → dann Flag-Default auf Staging-AN (analog `by_map_gl`). +- Download-Button auf der **Karte** (`map-offline-btn`) im GL-Modus auf `downloadAround(Karten-Center)` umbiegen + (bisher OSM-Raster-Prefetch). +- **Adaptives Lernen** (rollendes Vorausladen beim Aufzeichnen + Funkloch-Gedächtnis). +- **Bereichsauswahl / Routen-Korridor** (inkl. „Route offline speichern" aus routes.js `_openDetail`). +- **Glyph-Persistenz** über App-Updates (aktuell SW-Cache, wird bei Update gepurged) → in IndexedDB ablegen + via `byt://f/` servieren. +- Alten OSM-Raster-Prefetch (`offline-indicator.js _prefetchTiles`) entfernen, wenn Flag dauerhaft AN. ## Ziel GL-Vektorkarten offline-tauglich machen — Kernszenario **Gassi/Wandern im Funkloch**. diff --git a/tools/osm-extract/refresh.sh b/tools/osm-extract/refresh.sh index a2f9ec9..903ed27 100644 --- a/tools/osm-extract/refresh.sh +++ b/tools/osm-extract/refresh.sh @@ -11,9 +11,7 @@ set -euo pipefail DB="${DB_PATH:-/data/banyaro.db}" WORK="${WORK_DIR:-/work}" -# Default = TILES_REGIONS im Makefile (Karten- und POI-Abdeckung synchron halten). -# Produktiv setzt docker-compose.osm.yml die Liste zusätzlich per Env. -COUNTRIES="${COUNTRIES:-germany austria switzerland france italy czech-republic poland slovakia hungary slovenia netherlands belgium luxembourg denmark liechtenstein}" +COUNTRIES="${COUNTRIES:-switzerland austria germany}" GEOFABRIK="${GEOFABRIK_BASE:-https://download.geofabrik.de/europe}" KEEP_BACKUPS="${KEEP_BACKUPS:-3}" PREBUILT_SQLITE="${PREBUILT_SQLITE:-}"