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 @@
+
+
@@ -612,11 +617,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -626,7 +631,7 @@
-
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index 468016a..a55f2a6 100644
--- a/backend/static/js/app.js
+++ b/backend/static/js/app.js
@@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
-const APP_VER = '1228'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1220'; // ← 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;
@@ -1123,12 +1123,7 @@ const App = (() => {
const _rawHash = location.hash.replace('#', '');
const _hashQuery = _rawHash.split('?')[1] || '';
const _hashP = new URLSearchParams(_hashQuery);
- // Rechtsseiten direkt verlinkt (iOS-App, App-Store-Metadaten, E-Mails,
- // /impressum-Redirects) → ebenfalls in App bleiben statt /info
- const _hashPage = _rawHash.split('?')[0];
- const _legalPages = ['impressum', 'datenschutz', 'agb'];
- if (_hashP.get('verified') || _hashP.get('token') || location.pathname.startsWith('/teilen/')
- || _legalPages.includes(_hashPage)) {
+ if (_hashP.get('verified') || _hashP.get('token') || location.pathname.startsWith('/teilen/')) {
sessionStorage.setItem('by_stay_in_app', '1');
}
@@ -1203,11 +1198,7 @@ const App = (() => {
}
const startPage = (hashPage && pages[hashPage]) ? hashPage : 'welcome';
- // Hash-Route auch ohne Login ansteuern — öffentliche Seiten (AGB,
- // Datenschutz, Impressum, …) müssen für anonyme Besucher erreichbar sein.
- // Auth-pflichtige Seiten leitet navigate() über den requiresAuth-Guard
- // selbst auf 'welcome' um.
- navigate(startPage, false, hashParams);
+ navigate(state.user ? startPage : 'welcome', false, hashParams);
if (window.Worlds && state.user) window.Worlds.init(state);
}
diff --git a/backend/static/js/boot.js b/backend/static/js/boot.js
index 09e1624..85becee 100644
--- a/backend/static/js/boot.js
+++ b/backend/static/js/boot.js
@@ -24,44 +24,17 @@
// MapLibre-GL-Karte (zentrale Karte) aus ?mapgl=1/0 — wird in pages/map.js _useGL() ausgewertet.
var mg = new URLSearchParams(location.search).get('mapgl');
if (mg !== null) localStorage.setItem('by_map_gl', mg === '0' ? '0' : '1');
- // Offline-Vektorkacheln (byt://) aus ?tilesoffline=1/0 — ausgewertet via BY.offlineTiles().
- var to = new URLSearchParams(location.search).get('tilesoffline');
- if (to !== null) localStorage.setItem('by_offline_tiles', to === '0' ? '0' : '1');
} catch (e) {}
})();
-// ----------------------------------------------------------
-// Zentrale Feature-Flag-Helper (boot.js lädt vor allen Modulen)
-// ----------------------------------------------------------
-window.BY = window.BY || {};
-// Offline-Vektorkacheln (byt://): Default AN auf allen deployten Hosts (Prod + Staging),
-// localhost bleibt AUS; localStorage by_offline_tiles '1'/'0' bzw. ?tilesoffline übersteuert.
-// Prod-Freigabe René 2026-06-07 (analog by_map_gl, Gerätetests Runde 1+2 bestanden).
-window.BY.offlineTiles = function () {
- try {
- var flag = localStorage.getItem('by_offline_tiles');
- if (flag === '1') return true;
- if (flag === '0') return false;
- return /(^|\.)banyaro\.(app|de)$/.test(location.hostname);
- } catch (e) { return false; }
-};
-
// ----------------------------------------------------------
// Offline-Banner
// ----------------------------------------------------------
(function() {
- var _collapseTimer = null;
function _updateBanner() {
var banner = document.getElementById('offline-banner');
if (!banner) return;
- clearTimeout(_collapseTimer);
- banner.classList.remove('collapsed');
banner.style.display = navigator.onLine ? 'none' : 'flex';
- // Nach 5s auf schmale Icon-Leiste einklappen — das volle Banner verdeckt
- // sonst die Steuerung oben (z.B. Karten-Legende; Gerätetest iOS 2026-06-06).
- if (!navigator.onLine) {
- _collapseTimer = setTimeout(function() { banner.classList.add('collapsed'); }, 5000);
- }
}
window.addEventListener('offline', function() {
_updateBanner();
diff --git a/backend/static/js/landing-init.js b/backend/static/js/landing-init.js
index ada2d1d..b4bcb53 100644
--- a/backend/static/js/landing-init.js
+++ b/backend/static/js/landing-init.js
@@ -64,8 +64,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Live-Zahlen
var fmt = new Intl.NumberFormat('de-DE');
fetch('/api/stats/public')
- // r.ok prüfen: der SW antwortet offline mit 503+JSON ({detail:…}) → json() wirft nicht
- .then(function(r) { if (!r.ok) throw new Error('stats ' + r.status); return r.json(); })
+ .then(function(r) { return r.json(); })
.then(function(d) {
function set(id, val) {
var el = document.getElementById(id);
diff --git a/backend/static/js/map-gl-style.js b/backend/static/js/map-gl-style.js
index 0dab253..9a02bc9 100644
--- a/backend/static/js/map-gl-style.js
+++ b/backend/static/js/map-gl-style.js
@@ -12,9 +12,10 @@
var TILES_VER = '20260605';
function tilesUrl() { return window.location.origin + '/tiles/' + TILES_FILE + '?v=' + TILES_VER; }
- // Offline-Tiles-Modus (byt://-Quelle) — zentrale Flag-Logik in boot.js BY.offlineTiles().
+ // Offline-Tiles-Modus (byt://-Quelle). Opt-in via localStorage by_offline_tiles='1' bzw. ?tilesoffline=1.
+ // Default AUS, bis auf Gerät verifiziert — dann hier auf Staging-Default umstellen (analog by_map_gl).
function _offlineEnabled() {
- try { return !!(window.BY && BY.offlineTiles()); } catch (e) { return false; }
+ try { return localStorage.getItem('by_offline_tiles') === '1'; } catch (e) { return false; }
}
var THEMES = {
@@ -51,11 +52,7 @@
: { type: 'vector', url: 'pmtiles://' + tilesUrl() };
return {
version: 8,
- // Offline-Modus: Glyphs übers byt://f/-Protokoll (IndexedDB-first, remote-Fallback) —
- // der SW-Cache für /fonts wird bei App-Updates gepurged, IndexedDB nicht.
- glyphs: useOffline
- ? 'byt://f/{fontstack}/{range}'
- : window.location.origin + '/fonts/{fontstack}/{range}.pbf',
+ glyphs: window.location.origin + '/fonts/{fontstack}/{range}.pbf',
sources: {
by: src,
},
diff --git a/backend/static/js/map-offline.js b/backend/static/js/map-offline.js
index 127c286..76741ee 100644
--- a/backend/static/js/map-offline.js
+++ b/backend/static/js/map-offline.js
@@ -38,8 +38,6 @@ window.MapOffline = (function () {
var _get = function (k) { return _req(STORE, 'readonly', function (os) { return os.get(k); }); };
var _put = function (k, v) { return _req(STORE, 'readwrite', function (os) { os.put(v, k); }); };
var _count = function () { return _req(STORE, 'readonly', function (os) { return os.count(); }); };
- var _metaGet = function (k) { return _req(META, 'readonly', function (os) { return os.get(k); }); };
- var _metaPut = function (k, v) { return _req(META, 'readwrite', function (os) { os.put(v, k); }); };
// ---- Remote-PMTiles ----
function _pmInst() { if (!_pm) _pm = new pmtiles.PMTiles(MapGLStyle.tilesUrl()); return _pm; }
@@ -50,29 +48,21 @@ window.MapOffline = (function () {
if (hit) return hit instanceof Uint8Array ? hit : new Uint8Array(hit);
return _pmInst().getZxy(z, x, y).then(function (r) {
return (r && r.data) ? new Uint8Array(r.data) : null; // getZxy ist bereits dekomprimiert
- }).catch(function () {
- _noteRemoteMiss(); // Funkloch-Signal: echter Fetch-Fehler bei aktivem GPS
- return null; // offline + nicht gespeichert → leeres Tile
- });
+ }).catch(function () { return null; }); // offline + nicht gespeichert → leeres Tile
});
}
- // MapLibre-Protokolle registrieren (idempotent):
- // byt://t/{z}/{x}/{y} → Vektorkachel (MVT)
- // byt://f/{fontstack}/{range} → Glyph-PBF (fontstack ggf. URL-encodiert, je nach MapLibre-Version)
+ // MapLibre-Protokoll `byt://t/{z}/{x}/{y}` registrieren (idempotent).
function registerProtocol() {
if (registerProtocol._done || typeof maplibregl === 'undefined') return;
registerProtocol._done = true;
maplibregl.addProtocol('byt', function (params) {
- var ret = function (u) {
+ var m = /byt:\/\/t\/(\d+)\/(\d+)\/(\d+)/.exec(params.url);
+ if (!m) return Promise.resolve({ data: new ArrayBuffer(0) });
+ return tile(+m[1], +m[2], +m[3]).then(function (u) {
if (!u) return { data: new ArrayBuffer(0) };
return { data: u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength) };
- };
- var t = /byt:\/\/t\/(\d+)\/(\d+)\/(\d+)/.exec(params.url);
- if (t) return tile(+t[1], +t[2], +t[3]).then(ret);
- var f = /byt:\/\/f\/([^/]+)\/(\d+-\d+)/.exec(params.url);
- if (f) return glyph(decodeURIComponent(f[1]), f[2]).then(ret);
- return Promise.resolve({ data: new ArrayBuffer(0) });
+ });
});
}
@@ -82,360 +72,60 @@ window.MapOffline = (function () {
var r = lat * Math.PI / 180;
return Math.floor((1 - Math.log(Math.tan(r) + 1 / Math.cos(r)) / Math.PI) / 2 * Math.pow(2, z));
}
- function _bboxAround(lat, lon, radiusKm) {
+ function _tileList(lat, lon, radiusKm) {
var dLat = radiusKm / 111, dLon = radiusKm / (111 * Math.cos(lat * Math.PI / 180));
- return { south: lat - dLat, west: lon - dLon, north: lat + dLat, east: lon + dLon };
+ var w = lon - dLon, e = lon + dLon, s = lat - dLat, n = lat + dLat, list = [];
+ for (var z = 0; z <= MAXZOOM; z++) {
+ var x0 = _x(w, z), x1 = _x(e, z), y0 = _y(n, z), y1 = _y(s, z);
+ for (var x = x0; x <= x1; x++) for (var y = y0; y <= y1; y++) list.push([z, x, y]);
+ }
+ return list;
}
- // Glyphs (Open Sans Regular/Semibold, Latin + Latin-Extended) in IndexedDB persistieren
- // (Key-Präfix 'f/' im Tiles-Store, kein Schema-Bump) — überlebt App-Updates, anders als der
- // SW-Cache, der beim Update gepurged wird. Offline serviert übers byt://f/-Protokoll.
+ // Glyphs (Open Sans Regular/Semibold, Latin + Latin-Extended) holen, damit der Service-Worker sie cacht.
// KRITISCH: ohne Glyphs lässt MapLibre offline die GANZE Kachel fallen (nicht nur die Labels) → leer.
- // 0-255 + 256-511 deckt DE/FR/PL/CZ/IT-Sonderzeichen ab.
+ // 0-255 + 256-511 deckt DE/FR/PL/CZ/IT-Sonderzeichen ab. (Persistenz über App-Updates = Follow-up.)
var FONTS = ['Open Sans Regular', 'Open Sans Semibold'], RANGES = ['0-255', '256-511'];
- function _glyphUrl(font, range) { return '/fonts/' + encodeURIComponent(font) + '/' + range + '.pbf'; }
function _cacheGlyphs() {
var bytes = 0, jobs = [];
FONTS.forEach(function (f) { RANGES.forEach(function (rg) {
- jobs.push(fetch(_glyphUrl(f, rg))
+ jobs.push(fetch('/fonts/' + encodeURIComponent(f) + '/' + rg + '.pbf')
.then(function (r) { return r.ok ? r.arrayBuffer() : null; })
- .then(function (b) {
- if (!b) return;
- bytes += b.byteLength;
- return _put('f/' + f + '/' + rg, new Uint8Array(b));
- })
+ .then(function (b) { if (b) bytes += b.byteLength; })
.catch(function () {}));
}); });
return Promise.all(jobs).then(function () { return bytes; });
}
- // Glyph-Bytes für fontstack/range — IndexedDB zuerst, sonst remote (online), sonst null.
- function glyph(font, range) {
- return _get('f/' + font + '/' + range).then(function (hit) {
- if (hit) return hit instanceof Uint8Array ? hit : new Uint8Array(hit);
- return fetch(_glyphUrl(font, range))
- .then(function (r) { return r.ok ? r.arrayBuffer() : null; })
- .then(function (b) { return b ? new Uint8Array(b) : null; })
- .catch(function () { return null; });
- });
- }
-
- // POI-Marker der Region mitspeichern (Key-Präfix 'p/
' im Tiles-Store) — sonst steht die
- // Offline-Karte ohne Marker da (Gerätetest 2026-06-06). Quelle: /api/osm/pois (liest die lokale
- // osm_pois-DB, fast=true). Typen = Werte von OSM_LAYER_MAP in pages/map.js (synchron halten).
- var POI_TYPES = ['waste_basket', 'dog_park', 'drinking_water', 'tierarzt', 'hundesalon', 'shop',
- 'restaurant', 'bank', 'giftkoeder', 'kotbeutel', 'gefahr', 'parkplatz',
- 'treffpunkt', 'sonstiges', 'hotel'];
-
- // Frische Liste mit Bestand mergen (per id) — eine zweite Region (Urlaubsort) darf die erste
- // nicht löschen. Liefert die Anzahl frischer Einträge.
- function _mergeStore(key, fresh) {
- if (!fresh || !fresh.length) return Promise.resolve(0);
- return _get(key).then(function (old) {
- var merged = fresh;
- if (old && old.length) {
- var seen = {};
- fresh.forEach(function (p) { seen[p.id] = true; });
- merged = fresh.concat(old.filter(function (p) { return !seen[p.id]; }));
- }
- return _put(key, merged).then(function () { return fresh.length; });
- });
- }
-
- function _cachePois(bbox) {
- var total = 0;
- var jobs = POI_TYPES.map(function (type) {
- var params = new URLSearchParams({ type: type, fast: 'true',
- south: bbox.south, west: bbox.west, north: bbox.north, east: bbox.east });
- return fetch('/api/osm/pois?' + params)
- .then(function (r) { return r.ok ? r.json() : null; })
- .then(function (fresh) { return _mergeStore('p/' + type, fresh); })
- .then(function (n) { total += n; })
- .catch(function () {});
- });
-
- // Sicherheitsdaten MÜSSEN offline da sein (René 2026-06-07): Giftköder-Alarme
- // (/api/poison, Radius in m) + vermisste Hunde (/api/lost, Radius in km) — beide anonym.
- var midLat = (bbox.south + bbox.north) / 2, midLon = (bbox.west + bbox.east) / 2;
- var radiusKm = Math.max(
- (bbox.north - bbox.south) * 111 / 2,
- (bbox.east - bbox.west) * 111 * Math.cos(midLat * Math.PI / 180) / 2, 1);
- jobs.push(fetch('/api/poison?lat=' + midLat + '&lon=' + midLon + '&radius=' + Math.round(radiusKm * 1000))
- .then(function (r) { return r.ok ? r.json() : null; })
- .then(function (fresh) { return _mergeStore('p/_poison', fresh); })
- .then(function (n) { total += n; })
- .catch(function () {}));
- jobs.push(fetch('/api/lost?lat=' + midLat + '&lon=' + midLon + '&radius_km=' + Math.ceil(radiusKm))
- .then(function (r) { return r.ok ? r.json() : null; })
- .then(function (fresh) { return _mergeStore('p/_lost', fresh); })
- .then(function (n) { total += n; })
- .catch(function () {}));
-
- return Promise.all(jobs).then(function () { return total; });
- }
-
- // Gespeicherte Sicherheits-Alarme ('poison' | 'lost') im Bbox-Ausschnitt — Offline-Fallback.
- function alerts(kind, bbox) {
- return _get('p/_' + kind).then(function (list) {
- if (!list || !list.length) return [];
- return list.filter(function (p) {
- return p.lat >= bbox.south && p.lat <= bbox.north && p.lon >= bbox.west && p.lon <= bbox.east;
- });
- }).catch(function () { return []; });
- }
-
- // Gespeicherte POIs eines Typs im Bbox-Ausschnitt — Offline-Fallback für die Karten-Marker.
- function pois(type, bbox) {
- return _get('p/' + type).then(function (list) {
- if (!list || !list.length) return [];
- return list.filter(function (p) {
- return p.lat >= bbox.south && p.lat <= bbox.north && p.lon >= bbox.west && p.lon <= bbox.east;
- });
- }).catch(function () { return []; });
- }
-
- // Kachelbreite (km) auf Zoom z bei lat — für Ring↔Radius-Umrechnung.
- function _tileKm(z, lat) { return 40075 * Math.cos(lat * Math.PI / 180) / Math.pow(2, z); }
-
- // Kachel-Liste laden (Concurrency 6) — addiert auf state {done,bytes,stored}, ruft onTick alle 8 Tiles.
- function _fetchTiles(list, state, onTick) {
- var i = 0, CONC = 6;
+ // Bereich um lat/lon (radiusKm, Default 5) herunterladen + in IndexedDB ablegen.
+ // onProgress({done,total,bytes}). Liefert {tiles,bytes}.
+ function downloadAround(lat, lon, radiusKm, onProgress) {
+ radiusKm = radiusKm || 5;
+ var list = _tileList(lat, lon, radiusKm), total = list.length, done = 0, bytes = 0, stored = 0, i = 0, CONC = 6;
function next() {
- if (i >= list.length) return Promise.resolve();
+ if (i >= total) return Promise.resolve();
var t = list[i++], key = t[0] + '/' + t[1] + '/' + t[2];
return _pmInst().getZxy(t[0], t[1], t[2]).then(function (r) {
- if (r && r.data) { var u = new Uint8Array(r.data); state.bytes += u.byteLength; state.stored++; return _put(key, u); }
+ if (r && r.data) { var u = new Uint8Array(r.data); bytes += u.byteLength; stored++; return _put(key, u); }
}).catch(function () {}).then(function () {
- state.done++;
- if (onTick && state.done % 8 === 0) onTick();
+ done++;
+ if (onProgress && (done % 8 === 0 || done === total)) onProgress({ done: done, total: total, bytes: bytes });
return next();
});
}
var w = []; for (var k = 0; k < CONC; k++) w.push(next());
- return Promise.all(w);
- }
-
- // Gespeichertes Gebiet in der Regions-Liste verbuchen ('region' = letztes, Back-Compat).
- function _addRegion(region) {
- return _metaGet('regions').then(function (list) {
- list = list || [];
- list.push(region);
- if (list.length > 30) list = list.slice(-30);
- return _metaPut('regions', list);
- }).then(function () { return _metaPut('region', region); });
- }
-
- // Gebiet um lat/lon BUDGET-getrieben laden (Modell René 2026-06-07): z14-Ringe um den
- // Standort expandieren (+ Eltern-Kacheln z10–13), bis budgetMB GESPEICHERTE Bytes erreicht
- // sind — deckt in der Stadt einen kleineren, auf dem Land einen größeren Bereich ab
- // (dort sind die Funklöcher). Basis-Zooms 0–9 sind immer dabei (winzig).
- // opts {budgetMB:5, maxRadiusKm:25, onProgress({bytes,budget,done,radiusKm})}.
- // Liefert {tiles, bytes, pois, radiusKm}.
- function downloadAround(lat, lon, opts) {
- if (typeof opts === 'number') opts = {}; // alte Signatur (lat, lon, radiusKm) → Default-Budget
- opts = opts || {};
- var budget = (opts.budgetMB || 5) * 1048576;
- var maxKm = opts.maxRadiusKm || 25;
- var cx = _x(lon, MAXZOOM), cy = _y(lat, MAXZOOM);
- var kmPerTile = _tileKm(MAXZOOM, lat);
- var maxRing = Math.max(1, Math.ceil(maxKm / kmPerTile));
- var seen = {}, state = { done: 0, bytes: 0, stored: 0 };
- var tick = function (radiusKm) {
- if (opts.onProgress) opts.onProgress({ bytes: state.bytes, budget: budget, done: state.done, radiusKm: radiusKm });
- };
-
- // Basis-Zooms 0–9 über die maximale Bbox.
- var base = [], bb = _bboxAround(lat, lon, maxKm);
- for (var z = 0; z <= 9; z++) {
- var x0 = _x(bb.west, z), x1 = _x(bb.east, z), y0 = _y(bb.north, z), y1 = _y(bb.south, z);
- for (var x = x0; x <= x1; x++) for (var y = y0; y <= y1; y++) {
- var k = z + '/' + x + '/' + y;
- if (!seen[k]) { seen[k] = 1; base.push([z, x, y]); }
- }
- }
-
- // Ring r um die Zentrums-Kachel (z14) + zugehörige Eltern z10–13.
- function ringTiles(r) {
- var list = [];
- var push = function (z2, x2, y2) {
- if (x2 < 0 || y2 < 0 || x2 >= Math.pow(2, z2) || y2 >= Math.pow(2, z2)) return;
- var k2 = z2 + '/' + x2 + '/' + y2;
- if (!seen[k2]) { seen[k2] = 1; list.push([z2, x2, y2]); }
- };
- for (var x2 = cx - r; x2 <= cx + r; x2++) for (var y2 = cy - r; y2 <= cy + r; y2++) {
- if (Math.max(Math.abs(x2 - cx), Math.abs(y2 - cy)) !== r) continue;
- push(MAXZOOM, x2, y2);
- for (var pz = 13; pz >= 10; pz--) push(pz, x2 >> (MAXZOOM - pz), y2 >> (MAXZOOM - pz));
- }
- return list;
- }
-
- var coveredRing = 0;
- function nextRing(r) {
- if (r > maxRing || state.bytes >= budget) return Promise.resolve();
- return _fetchTiles(ringTiles(r), state, function () { tick(r * kmPerTile); }).then(function () {
- coveredRing = r;
- tick(r * kmPerTile);
- return nextRing(r + 1);
- });
- }
-
- var radiusKm = 0, poiCount = 0;
- return _fetchTiles(base, state, function () { tick(0); })
- .then(function () { return nextRing(0); })
- .then(function () {
- radiusKm = Math.max(1, Math.round(coveredRing * kmPerTile * 10) / 10);
- return _cacheGlyphs(); // Glyphs mitcachen (sonst offline kein Render)
- })
- .then(function (gb) { state.bytes += gb; return _cachePois(_bboxAround(lat, lon, radiusKm)); })
- .then(function (pc) {
- poiCount = pc;
- return _addRegion({ lat: lat, lon: lon, radiusKm: radiusKm, tiles: state.stored,
- bytes: state.bytes, pois: poiCount, savedAt: Date.now() });
- })
- .then(function () { return { tiles: state.stored, bytes: state.bytes, pois: poiCount, radiusKm: radiusKm }; });
- }
-
- // ---- Funkloch-Gedächtnis ----------------------------------------------------
- // „Wo verliere ich Netz" = Aufenthaltsorte → bleibt KOMPLETT LOKAL (IndexedDB),
- // wird nie hochgeladen. Signal = echte Tile-Fetch-Fehler bei aktivem GPS
- // (NICHT navigator.onLine — das lügt bei Captive-Portal/Schwachempfang).
- var _gps = null, _lastZoneNote = 0;
- function setGps(pos) { _gps = pos; } // {lat,lon} während aktiver Aufzeichnung, sonst null
-
- function _distKm(aLat, aLon, bLat, bLon) {
- var dLat = (bLat - aLat) * 111, dLon = (bLon - aLon) * 111 * Math.cos(aLat * Math.PI / 180);
- return Math.sqrt(dLat * dLat + dLon * dLon);
- }
-
- function _noteRemoteMiss() {
- if (!_gps) return;
- var now = Date.now();
- if (now - _lastZoneNote < 120000) return; // max. 1 Eintrag / 2 Min
- _lastZoneNote = now;
- markDeadZone(_gps.lat, _gps.lon);
- }
-
- // Funkloch merken (Dedupe: keine zweite Zone im Umkreis von 2 km, Cap 50).
- function markDeadZone(lat, lon) {
- return _metaGet('deadzones').then(function (zones) {
- zones = zones || [];
- if (zones.some(function (z) { return _distKm(z.lat, z.lon, lat, lon) < 2; })) return;
- zones.push({ lat: lat, lon: lon, ts: Date.now(), filled: false });
- if (zones.length > 50) zones = zones.slice(-50);
- return _metaPut('deadzones', zones);
- }).catch(function () {});
- }
-
- // Offene Funkloch-Zonen budget-getrieben nachladen (nur online sinnvoll).
- // Dort wo Netz verfügbar ist, braucht man keine Offline-Karten — gespeichert
- // wird gezielt da, wo es ausfiel. Liefert die Anzahl gefüllter Zonen.
- var _autofillActive = false;
- function autoFillDeadZones() {
- if (_autofillActive || !navigator.onLine) return Promise.resolve(0);
- _autofillActive = true;
- var filled = 0;
- return _metaGet('deadzones').then(function (zones) {
- zones = zones || [];
- var open = zones.filter(function (z) { return !z.filled; });
- if (!open.length) return 0;
- var chain = Promise.resolve();
- open.forEach(function (z) {
- chain = chain.then(function () {
- return downloadAround(z.lat, z.lon, { budgetMB: 5 }).then(function (res) {
- if (res.bytes > 0) { z.filled = true; filled++; }
- }).catch(function () {});
- });
- });
- return chain.then(function () { return _metaPut('deadzones', zones); })
- .then(function () { return filled; });
- }).then(function (n) { _autofillActive = false; return n; },
- function () { _autofillActive = false; return 0; });
- }
-
- // ---- Routen-Korridor ---------------------------------------------------------
- // Kacheln ±bufferKm um den Track (z14 + Eltern z10–13 + Basis 0–9) + Marker der
- // Korridor-Bbox — für mehrtägige Unternehmungen entlang einer Route.
- // track = [{lat,lon},...]; opts {bufferKm:1, capMB:50, name, onProgress({bytes,done,total})}.
- function downloadCorridor(track, opts) {
- opts = opts || {};
- if (!track || track.length < 2) return Promise.reject(new Error('Kein GPS-Track'));
- var buffer = opts.bufferKm || 1, cap = (opts.capMB || 50) * 1048576;
- var seen = {}, list = [];
- var push = function (z, x, y) {
- if (x < 0 || y < 0 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) return;
- var k = z + '/' + x + '/' + y;
- if (!seen[k]) { seen[k] = 1; list.push([z, x, y]); }
- };
- var s = 90, w = 180, n = -90, e = -180;
- track.forEach(function (p) {
- if (p.lat < s) s = p.lat; if (p.lat > n) n = p.lat;
- if (p.lon < w) w = p.lon; if (p.lon > e) e = p.lon;
- var d = Math.ceil(buffer / _tileKm(MAXZOOM, p.lat));
- var cx = _x(p.lon, MAXZOOM), cy = _y(p.lat, MAXZOOM);
- for (var x = cx - d; x <= cx + d; x++) for (var y = cy - d; y <= cy + d; y++) {
- push(MAXZOOM, x, y);
- for (var pz = 13; pz >= 10; pz--) push(pz, x >> (MAXZOOM - pz), y >> (MAXZOOM - pz));
- }
- });
- var latMid = (s + n) / 2;
- var bb = { south: s - buffer / 111, north: n + buffer / 111,
- west: w - buffer / (111 * Math.cos(latMid * Math.PI / 180)),
- east: e + buffer / (111 * Math.cos(latMid * Math.PI / 180)) };
- for (var z2 = 0; z2 <= 9; z2++) {
- var x0 = _x(bb.west, z2), x1 = _x(bb.east, z2), y0 = _y(bb.north, z2), y1 = _y(bb.south, z2);
- for (var x2 = x0; x2 <= x1; x2++) for (var y2 = y0; y2 <= y1; y2++) push(z2, x2, y2);
- }
-
- var state = { done: 0, bytes: 0, stored: 0 }, total = list.length, poiCount = 0;
- // In 64er-Blöcken laden → zwischen den Blöcken greift der Speicher-Cap.
- function chunkLoop(idx) {
- if (idx >= list.length || state.bytes >= cap) return Promise.resolve();
- return _fetchTiles(list.slice(idx, idx + 64), state, function () {
- if (opts.onProgress) opts.onProgress({ bytes: state.bytes, done: state.done, total: total });
- }).then(function () { return chunkLoop(idx + 64); });
- }
- return chunkLoop(0)
- .then(function () { return _cacheGlyphs(); })
- .then(function (gb) { state.bytes += gb; return _cachePois(bb); })
- .then(function (pc) {
- poiCount = pc;
- return _addRegion({ type: 'korridor', name: opts.name || null, lat: track[0].lat, lon: track[0].lon,
- tiles: state.stored, bytes: state.bytes, pois: poiCount, savedAt: Date.now() });
- })
- .then(function () { return { tiles: state.stored, bytes: state.bytes, pois: poiCount, capped: state.bytes >= cap }; });
- }
-
- // ---- Coverage-Layer ----------------------------------------------------------
- // GeoJSON der gespeicherten z14-Kacheln — zeigt auf der Karte, welche Bereiche offline da sind.
- function _tile2lat(y, z) {
- var m = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
- return 180 / Math.PI * Math.atan(0.5 * (Math.exp(m) - Math.exp(-m)));
- }
- function coverage() {
- var re = new RegExp('^' + MAXZOOM + '/(\\d+)/(\\d+)$');
- return _req(STORE, 'readonly', function (os) { return os.getAllKeys(); }).then(function (keys) {
- var feats = [];
- (keys || []).forEach(function (k) {
- var m = re.exec(k);
- if (!m) return;
- var x = +m[1], y = +m[2], n2 = Math.pow(2, MAXZOOM);
- var west = x / n2 * 360 - 180, east = (x + 1) / n2 * 360 - 180;
- var north = _tile2lat(y, MAXZOOM), south = _tile2lat(y + 1, MAXZOOM);
- feats.push({ type: 'Feature', properties: {}, geometry: { type: 'Polygon',
- coordinates: [[[west, south], [east, south], [east, north], [west, north], [west, south]]] } });
- });
- return { type: 'FeatureCollection', features: feats };
- });
+ return Promise.all(w)
+ .then(function () { return _cacheGlyphs(); }) // Glyphs mitcachen (sonst offline kein Render)
+ .then(function (gb) { bytes += gb; return _req(META, 'readwrite', function (os) {
+ os.put({ lat: lat, lon: lon, radiusKm: radiusKm, tiles: stored, bytes: bytes, savedAt: Date.now() }, 'region');
+ }); })
+ .then(function () { return { tiles: stored, bytes: bytes }; });
}
function stats() {
return _count().then(function (count) {
- return _metaGet('regions').then(function (regions) {
- return _metaGet('region').then(function (meta) {
- return { count: count, meta: meta || null, regions: regions || [] };
- });
- });
+ return _req(META, 'readonly', function (os) { return os.get('region'); })
+ .then(function (meta) { return { count: count, meta: meta || null }; });
});
}
function hasRegion() { return stats().then(function (s) { return s.count > 0; }).catch(function () { return false; }); }
@@ -445,9 +135,7 @@ window.MapOffline = (function () {
}
return {
- registerProtocol: registerProtocol, downloadAround: downloadAround, downloadCorridor: downloadCorridor,
- tile: tile, glyph: glyph, pois: pois, alerts: alerts, coverage: coverage,
- setGps: setGps, markDeadZone: markDeadZone, autoFillDeadZones: autoFillDeadZones,
+ registerProtocol: registerProtocol, downloadAround: downloadAround, tile: tile,
stats: stats, hasRegion: hasRegion, clear: clear, MAXZOOM: MAXZOOM,
};
})();
diff --git a/backend/static/js/offline-indicator.js b/backend/static/js/offline-indicator.js
index cb6bf6e..55e93b1 100644
--- a/backend/static/js/offline-indicator.js
+++ b/backend/static/js/offline-indicator.js
@@ -21,9 +21,8 @@ window.OfflineIndicator = (() => {
}
// GL-Offline-Tiles-Modus (byt://-Vektorkacheln in IndexedDB) statt OSM-Raster.
- // Zentrale Flag-Logik in boot.js BY.offlineTiles().
function _offlineTilesMode() {
- try { return !!(window.BY && BY.offlineTiles()); } catch (e) { return false; }
+ try { return localStorage.getItem('by_offline_tiles') === '1'; } catch (e) { return false; }
}
// Ist eine Offline-Region (Vektorkacheln) in IndexedDB gespeichert? (ohne MapOffline zu laden)
// WICHTIG: dasselbe Schema/Version wie map-offline.js anlegen — sonst legt ein versionsloses open()
@@ -207,7 +206,7 @@ window.OfflineIndicator = (() => {
await Promise.all(tasks);
}
- // GL-Offline: Gebiet (~5 MB budget-getrieben) um den aktuellen Standort in IndexedDB laden.
+ // GL-Offline: Vektor-Region (~5 km) um den aktuellen Standort in IndexedDB laden.
async function _downloadOfflineRegion() {
let pos = null;
try { pos = await API.getLocation(); } catch (e) {}
@@ -220,53 +219,10 @@ window.OfflineIndicator = (() => {
if (!pos) { UI.toast.warning('Standort nötig, um die Gegend offline zu speichern.'); return; }
try {
await UI.loadMapLibreUI();
- if (window.MapOffline) await MapOffline.downloadAround(pos.lat, pos.lon, { budgetMB: 5 });
+ if (window.MapOffline) await MapOffline.downloadAround(pos.lat, pos.lon, 5);
} catch (e) { console.warn('Offline-Region-Download fehlgeschlagen:', e); }
}
- // Gibt es offene (ungefüllte) Funkloch-Zonen? — direkt aus IndexedDB, OHNE den
- // GL-Stack zu laden (gleiches Schema/Version wie map-offline.js, s. Warnung oben).
- function _openDeadZonesStored() {
- return new Promise(res => {
- try {
- const r = indexedDB.open('by-offline-tiles', 1);
- r.onupgradeneeded = () => {
- const d = r.result;
- if (!d.objectStoreNames.contains('tiles')) d.createObjectStore('tiles');
- if (!d.objectStoreNames.contains('meta')) d.createObjectStore('meta');
- };
- r.onsuccess = () => {
- const db = r.result;
- if (!db.objectStoreNames.contains('meta')) { db.close(); return res(false); }
- const rq = db.transaction('meta', 'readonly').objectStore('meta').get('deadzones');
- rq.onsuccess = () => { res((rq.result || []).some(z => !z.filled)); db.close(); };
- rq.onerror = () => { res(false); db.close(); };
- };
- r.onerror = () => res(false);
- } catch (e) { res(false); }
- });
- }
-
- // Funkloch-Zonen automatisch füllen, sobald Netz da ist — das Gerät lernt selbst,
- // wo Offline-Karten nötig sind (dort wo Netz ist, braucht es keine).
- let _autoFillTimer = null;
- function _scheduleAutoFill(delayMs) {
- if (!_offlineTilesMode()) return;
- clearTimeout(_autoFillTimer);
- _autoFillTimer = setTimeout(async () => {
- if (!navigator.onLine) return;
- try {
- if (!(await _openDeadZonesStored())) return; // nichts zu tun → GL-Stack nicht laden
- await UI.loadMapLibreUI();
- const n = await window.MapOffline?.autoFillDeadZones?.();
- if (n) {
- UI.toast?.info(`${n} Funkloch-${n === 1 ? 'Gebiet' : 'Gebiete'} automatisch offline gespeichert.`);
- refresh();
- }
- } catch (e) {}
- }, delayMs);
- }
-
// ----------------------------------------------------------
// Tile-URL-Berechnung (OSM, Subdomain 'a')
// ----------------------------------------------------------
@@ -417,9 +373,7 @@ window.OfflineIndicator = (() => {
function init() {
refresh();
_prefetchPages();
- // OSM-Raster-Prefetch nur für die Leaflet-Karte — die GL-Karte (byt://-Vektorkacheln)
- // nutzt das Raster nicht. Komplett-Entfernung wenn Flag dauerhaft AN (OFFLINE_MAPS_PLAN.md).
- if (!_offlineTilesMode()) _prefetchTiles();
+ _prefetchTiles();
_prefetchData();
_bindLongPress();
@@ -435,9 +389,6 @@ window.OfflineIndicator = (() => {
if (e?.data?.type === 'CACHE_TILES_PROGRESS') refresh();
});
}
- // Funkloch-Zonen nachladen: verzögert beim Start + sobald die Verbindung zurückkommt.
- _scheduleAutoFill(30_000);
- window.addEventListener('online', () => _scheduleAutoFill(8_000));
_checkStorageQuota(); // beim Init prüfen
setInterval(() => { _prefetchData(); refresh(); _checkStorageQuota(); }, 60_000);
}
diff --git a/backend/static/js/pages/lost.js b/backend/static/js/pages/lost.js
index e2c92e9..30d18e5 100644
--- a/backend/static/js/pages/lost.js
+++ b/backend/static/js/pages/lost.js
@@ -233,25 +233,10 @@ window.Page_lost = (() => {
...p,
distanz_m: _haversine(_userPos.lat, _userPos.lon, p.lat, p.lon),
}));
- // Offline-Region-Snapshot (Offline-Karten speichern vermisste Hunde mit) dazu mergen —
- // deckt vorab gespeicherte Gegenden ab, die der localStorage-Stand nicht kennt.
- let regionLost = [];
- try {
- if (window.MapOffline?.alerts) {
- regionLost = await MapOffline.alerts('lost', {
- south: _userPos.lat - 0.3, north: _userPos.lat + 0.3,
- west: _userPos.lon - 0.45, east: _userPos.lon + 0.45,
- });
- }
- } catch {}
try {
const raw = localStorage.getItem(_CACHE_KEY);
if (raw) {
const cached = JSON.parse(raw).data || [];
- const seen = new Set(cached.map(r => r.id));
- regionLost.filter(r => !seen.has(r.id)).forEach(r => cached.push({
- ...r, distanz_m: _haversine(_userPos.lat, _userPos.lon, r.lat, r.lon),
- }));
_reports = [...offline_pending, ...cached];
_renderMarkers();
_renderHeld();
@@ -261,16 +246,12 @@ window.Page_lost = (() => {
return;
}
} catch {}
- // Kein localStorage-Stand → wenigstens Pending + Region-Snapshot zeigen
- _reports = [...offline_pending, ...regionLost.map(r => ({
- ...r, distanz_m: _haversine(_userPos.lat, _userPos.lon, r.lat, r.lon),
- }))];
- if (_reports.length) {
+ _reports = offline_pending;
+ if (offline_pending.length) {
_renderMarkers();
_renderHeld();
_renderList();
_updateBadge(_reports.length);
- if (infoEl) infoEl.textContent = 'Offline — zeige gespeicherte Meldungen.';
return;
}
UI.toast.error('Meldungen konnten nicht geladen werden.');
diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js
index e62f653..9f06461 100644
--- a/backend/static/js/pages/map.js
+++ b/backend/static/js/pages/map.js
@@ -257,11 +257,6 @@ window.Page_map = (() => {
Marker setzen
- ${(!_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:-}"