@@ -617,11 +612,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -631,7 +626,7 @@
-
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index a273e12..c47d15f 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 = '1222'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1223'; // ← 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;
diff --git a/backend/static/js/boot.js b/backend/static/js/boot.js
index 8a8421c..5245378 100644
--- a/backend/static/js/boot.js
+++ b/backend/static/js/boot.js
@@ -35,10 +35,18 @@
// 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/map-offline.js b/backend/static/js/map-offline.js
index 78db35c..5a3c408 100644
--- a/backend/static/js/map-offline.js
+++ b/backend/static/js/map-offline.js
@@ -77,11 +77,14 @@ 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 _tileList(lat, lon, radiusKm) {
+ function _bboxAround(lat, lon, radiusKm) {
var dLat = radiusKm / 111, dLon = radiusKm / (111 * Math.cos(lat * Math.PI / 180));
- var w = lon - dLon, e = lon + dLon, s = lat - dLat, n = lat + dLat, list = [];
+ return { south: lat - dLat, west: lon - dLon, north: lat + dLat, east: lon + dLon };
+ }
+ function _tileList(lat, lon, radiusKm) {
+ var b = _bboxAround(lat, lon, radiusKm), 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);
+ var x0 = _x(b.west, z), x1 = _x(b.east, z), y0 = _y(b.north, z), y1 = _y(b.south, z);
for (var x = x0; x <= x1; x++) for (var y = y0; y <= y1; y++) list.push([z, x, y]);
}
return list;
@@ -120,11 +123,54 @@ window.MapOffline = (function () {
});
}
+ // 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'];
+ 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) {
+ if (!fresh || !fresh.length) return;
+ total += fresh.length;
+ // Mit Bestand mergen (per id) — eine zweite Region (Urlaubsort) darf die erste nicht löschen.
+ return _get('p/' + type).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('p/' + type, merged);
+ });
+ })
+ .catch(function () {});
+ });
+ return Promise.all(jobs).then(function () { return total; });
+ }
+
+ // 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 []; });
+ }
+
// Bereich um lat/lon (radiusKm, Default 5) herunterladen + in IndexedDB ablegen.
- // onProgress({done,total,bytes}). Liefert {tiles,bytes}.
+ // onProgress({done,total,bytes}). Liefert {tiles,bytes,pois}.
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;
+ var poiCount = 0;
function next() {
if (i >= total) return Promise.resolve();
var t = list[i++], key = t[0] + '/' + t[1] + '/' + t[2];
@@ -139,10 +185,11 @@ window.MapOffline = (function () {
var w = []; for (var k = 0; k < CONC; k++) w.push(next());
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 (gb) { bytes += gb; return _cachePois(_bboxAround(lat, lon, radiusKm)); })
+ .then(function (pc) { poiCount = pc; return _req(META, 'readwrite', function (os) {
+ os.put({ lat: lat, lon: lon, radiusKm: radiusKm, tiles: stored, bytes: bytes, pois: poiCount, savedAt: Date.now() }, 'region');
}); })
- .then(function () { return { tiles: stored, bytes: bytes }; });
+ .then(function () { return { tiles: stored, bytes: bytes, pois: poiCount }; });
}
function stats() {
@@ -159,6 +206,6 @@ window.MapOffline = (function () {
return {
registerProtocol: registerProtocol, downloadAround: downloadAround, tile: tile, glyph: glyph,
- stats: stats, hasRegion: hasRegion, clear: clear, MAXZOOM: MAXZOOM,
+ pois: pois, stats: stats, hasRegion: hasRegion, clear: clear, MAXZOOM: MAXZOOM,
};
})();
diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js
index 75d0d30..dd159d0 100644
--- a/backend/static/js/pages/map.js
+++ b/backend/static/js/pages/map.js
@@ -1477,7 +1477,15 @@ window.Page_map = (() => {
const pois = await fetch(`/api/osm/pois?${params}`).then(r => r.json());
_replaceOsmMarkers(layerKey, pois);
return pois.length;
- } catch { return 0; }
+ } 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;
+ }
});
const fastCounts = await Promise.all(fastTasks);
const fastTotal = fastCounts.reduce((a, b) => a + b, 0);
@@ -2152,7 +2160,7 @@ window.Page_map = (() => {
_setOsmStatus(`Offline: ${Math.round(p.done / p.total * 100)} %…`);
});
_setOsmStatus('');
- UI.toast.success(`Gegend offline gespeichert — ${res.tiles} Kacheln, ${(res.bytes / 1048576).toFixed(1)} MB.`);
+ UI.toast.success(`Gegend offline gespeichert — ${res.tiles} Kacheln, ${res.pois || 0} Marker, ${(res.bytes / 1048576).toFixed(1)} MB.`);
window.OfflineIndicator?.refresh(); // Pfoten-Segment 5 sofort grün
} catch (e) {
_setOsmStatus('');
diff --git a/backend/static/landing.html b/backend/static/landing.html
index f4d7b6d..dc2f09a 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 f09fbd6..ab823c7 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 = '1222';
+const VER = '1223';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten