From 94a6ce49ba11d03cbbfb84ed7217d4736e4ade20 Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 6 Jun 2026 13:23:33 +0200 Subject: [PATCH 1/4] =?UTF-8?q?Offline-Karten=20Runde=206:=20Standort-Grun?= =?UTF-8?q?dversorgung=20=E2=80=94=20aktuelles=20Gebiet=20bleibt=20immer?= =?UTF-8?q?=20geladen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renes Modell Punkt 1 war zu eng interpretiert (nur Funkloecher): das Gebiet um die aktuelle Position gehoert zur Grundversorgung. - ensureHomeArea(lat,lon): Zentrums-Kachel-Check -> bei Luecke Budget-Download (type 'standort', Cap-gated) - Start-Check: Standort raw pruefen (ohne GL-Stack) und bei Bedarf laden - 'Alles loeschen': Standort-Gebiet wird SOFORT neu geladen (+ Toast) — vorher war die Offline-Funktionalitaet genau am wichtigsten Ort weg - Pfote Segment 5: Standort-Kachel da UND Zonen im 50-km-Umkreis gefuellt (ferne Zonen zaehlen nicht mehr — sie laden erst vor Ort) - Tests r6 + Regression r1/r3/r4/r5 gruen Bump v1234 --- VERSION | 2 +- backend/static/index.html | 24 ++++----- backend/static/js/app.js | 2 +- backend/static/js/map-offline.js | 17 +++++- backend/static/js/offline-indicator.js | 73 +++++++++++++++++++++++--- backend/static/js/pages/map.js | 11 +++- backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- docs/OFFLINE_MAPS_PLAN.md | 9 ++++ tests/js/README.md | 1 + tests/js/test-map-offline-r6.js | 68 ++++++++++++++++++++++++ 11 files changed, 185 insertions(+), 26 deletions(-) create mode 100644 tests/js/test-map-offline-r6.js diff --git a/VERSION b/VERSION index fb9a769..274c005 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1233 \ No newline at end of file +1234 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index 3b4a4bc..9079a4c 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -612,11 +612,11 @@ - - - - - + + + + + @@ -626,7 +626,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 513603f..c5890c0 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 = '1233'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1234'; // ← 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/map-offline.js b/backend/static/js/map-offline.js index 6d481a0..221fdfb 100644 --- a/backend/static/js/map-offline.js +++ b/backend/static/js/map-offline.js @@ -600,6 +600,21 @@ window.MapOffline = (function () { .then(function () { return { tiles: state.stored, bytes: state.bytes, pois: poiCount, capped: state.bytes >= cap }; }); } + // STANDORT-GRUNDVERSORGUNG (Renés Modell, Punkt 1): das Gebiet um die aktuelle Position + // ist IMMER geladen (~5 MB) — auch nach „Alles löschen", sonst ist die Offline- + // Funktionalität am wichtigsten Ort (hier!) weg. Zentrums-Kachel da → nichts zu tun. + function ensureHomeArea(lat, lon) { + return _get(MAXZOOM + '/' + _x(lon, MAXZOOM) + '/' + _y(lat, MAXZOOM)).then(function (hit) { + if (hit) return 0; + return _overCap().then(function (over) { + if (over) return 0; + return downloadAround(lat, lon, { budgetMB: 5, type: 'standort' }) + .then(function (res) { return res.bytes > 0 ? 1 : 0; }) + .catch(function () { return 0; }); + }); + }).catch(function () { return 0; }); + } + // Gespeicherte Routen offline nutzbar halten (René 2026-06-08): beim Start die Korridore // der eigenen Routen in Positionsnähe sicherstellen — Stichproben-Kacheln prüfen, bei // Lücken Korridor (neu) laden. Deckt „Alles löschen" + Eviction ab; preview_track @@ -753,7 +768,7 @@ window.MapOffline = (function () { return { registerProtocol: registerProtocol, downloadAround: downloadAround, downloadCorridor: downloadCorridor, - downloadBbox: downloadBbox, ensureRouteCorridors: ensureRouteCorridors, + downloadBbox: downloadBbox, ensureRouteCorridors: ensureRouteCorridors, ensureHomeArea: ensureHomeArea, tile: tile, glyph: glyph, pois: pois, alerts: alerts, coverage: coverage, setGps: setGps, markDeadZone: markDeadZone, removeDeadZone: removeDeadZone, autoFillDeadZones: autoFillDeadZones, 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 be73fe1..2e7e9df 100644 --- a/backend/static/js/offline-indicator.js +++ b/backend/static/js/offline-indicator.js @@ -25,9 +25,27 @@ window.OfflineIndicator = (() => { function _offlineTilesMode() { try { return !!(window.BY && BY.offlineTiles()); } catch (e) { return false; } } + // Slippy-Kachel-Koordinaten (z14, wie map-offline.js MAXZOOM) — für raw IDB-Checks + // ohne den GL-Stack zu laden. + function _tileKey14(lat, lon) { + const n = Math.pow(2, 14); + const x = Math.floor((lon + 180) / 360 * n); + const rad = lat * Math.PI / 180; + const y = Math.floor((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2 * n); + return `14/${x}/${y}`; + } + function _lsPos() { + try { + const raw = localStorage.getItem(LS_LAST_POS); + if (raw) { const p = JSON.parse(raw); if (p?.lat != null) return { lat: p.lat, lon: p.lon }; } + } catch {} + return null; + } + // Offline-Ready (Pfote Segment 5) — ohne MapOffline/GL-Stack zu laden. - // Semantik (Modell René 2026-06-08): Gibt es bekannte FUNKLOCH-Zonen, zählt deren - // Füllstand (alle gefüllt = grün); ohne bekannte Zonen wie bisher: irgendein Gebiet da. + // Semantik (Modell René 2026-06-08): Standort-Gebiet gespeichert UND alle bekannten + // Funkloch-Zonen IM UMKREIS (50 km, ferne zählen nicht — sie laden erst vor Ort) gefüllt. + // Ohne Position: irgendein Gebiet da. // WICHTIG: dasselbe Schema/Version wie map-offline.js anlegen — sonst legt ein versionsloses // open() die DB leer an und MapOffline kann seine Stores nicht mehr erstellen. function _offlineRegionStored() { @@ -44,12 +62,25 @@ window.OfflineIndicator = (() => { if (!db.objectStoreNames.contains('tiles') || !db.objectStoreNames.contains('meta')) { db.close(); return res(false); } + const pos = _lsPos(); const mz = db.transaction('meta', 'readonly').objectStore('meta').get('deadzones'); mz.onsuccess = () => { - const zones = mz.result || []; - if (zones.length) { res(zones.every(z => z.filled)); db.close(); return; } + const zones = (mz.result || []).filter(z => { + if (!pos) return true; + const dLat = (z.lat - pos.lat) * 111, + dLon = (z.lon - pos.lon) * 111 * Math.cos(pos.lat * Math.PI / 180); + return Math.sqrt(dLat * dLat + dLon * dLon) <= 50; + }); + const zonesOk = zones.every(z => z.filled); + if (pos) { + // Standort-Kachel vorhanden? (Grundversorgung) + const tq = db.transaction('tiles', 'readonly').objectStore('tiles').get(_tileKey14(pos.lat, pos.lon)); + tq.onsuccess = () => { res(!!tq.result && zonesOk); db.close(); }; + tq.onerror = () => { res(false); db.close(); }; + return; + } const cnt = db.transaction('tiles', 'readonly').objectStore('tiles').count(); - cnt.onsuccess = () => { res(cnt.result > 0); db.close(); }; + cnt.onsuccess = () => { res(cnt.result > 0 && zonesOk); db.close(); }; cnt.onerror = () => { res(false); db.close(); }; }; mz.onerror = () => { res(false); db.close(); }; @@ -59,6 +90,28 @@ window.OfflineIndicator = (() => { }); } + // Fehlt die Standort-Kachel? (raw, ohne GL-Stack) — Trigger für die Grundversorgung. + function _homeAreaMissing(pos) { + 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('tiles')) { db.close(); return res(true); } + const tq = db.transaction('tiles', 'readonly').objectStore('tiles').get(_tileKey14(pos.lat, pos.lon)); + tq.onsuccess = () => { res(!tq.result); db.close(); }; + tq.onerror = () => { res(true); db.close(); }; + }; + r.onerror = () => res(true); + } catch (e) { res(true); } + }); + } + const CHECKS = [ { step: 1, title: 'App-Grundgerüst', detail: 'CSS, Layout und Hauptmodule — die Basis', @@ -323,18 +376,22 @@ window.OfflineIndicator = (() => { _autoFillTimer = setTimeout(async () => { if (!navigator.onLine) return; try { + const pos = await _lastKnownPos(); const hasZones = await _anyDeadZonesStored(); + const homeMissing = pos ? await _homeAreaMissing(pos) : false; let routes = []; try { routes = (await API.routes.list()) || []; } catch {} routes = routes.filter(r => (r.preview_track || []).length >= 2); - if (!hasZones && !routes.length) return; // nichts zu tun → GL-Stack nicht laden - const pos = await _lastKnownPos(); + if (!hasZones && !routes.length && !homeMissing) return; // nichts zu tun → GL-Stack nicht laden const o = pos ? { lat: pos.lat, lon: pos.lon } : {}; await UI.loadMapLibreUI(); + // Standort-Grundversorgung zuerst (René: das Gebiet am aktuellen Standort ist IMMER da) + const h = (homeMissing && pos) ? (await window.MapOffline?.ensureHomeArea?.(pos.lat, pos.lon) || 0) : 0; const n = await window.MapOffline?.autoFillDeadZones?.(o) || 0; const k = routes.length ? (await window.MapOffline?.ensureRouteCorridors?.(routes, o) || 0) : 0; - if (n || k) { + if (h || n || k) { const parts = []; + if (h) parts.push('Standort-Gebiet'); if (n) parts.push(`${n} Funkloch-${n === 1 ? 'Gebiet' : 'Gebiete'}`); if (k) parts.push(`${k} Routen-${k === 1 ? 'Korridor' : 'Korridore'}`); UI.toast?.info(`${parts.join(' + ')} automatisch offline gespeichert.`); diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index 213713f..7569458 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -2311,7 +2311,16 @@ window.Page_map = (() => { await MapOffline.clear().catch(() => {}); _setCoverage(false); UI.modal.close(); - UI.toast.success('Offline-Karten gelöscht. Bekannte Funkloch-Gebiete werden beim nächsten Start automatisch neu geladen.'); + UI.toast.success('Offline-Karten gelöscht. Funkloch-Gebiete werden beim nächsten Start automatisch neu geladen.'); + // Standort-Grundversorgung sofort wiederherstellen (René 2026-06-08: das Gebiet am + // aktuellen Standort muss bleiben — es würde sonst nicht automatisch vorgeladen + // und die Offline-Funktionalität wäre genau hier weg). + if (_userPos && navigator.onLine) { + try { + const r = await MapOffline.ensureHomeArea(_userPos.lat, _userPos.lon); + if (r) UI.toast.info('Dein Standort-Gebiet wurde neu geladen — offline weiter verfügbar.'); + } catch (e) {} + } window.OfflineIndicator?.refresh(); }); } diff --git a/backend/static/landing.html b/backend/static/landing.html index 31aadcf..2266243 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 e514ca9..cb819ee 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 = '1233'; +const VER = '1234'; 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/docs/OFFLINE_MAPS_PLAN.md b/docs/OFFLINE_MAPS_PLAN.md index 74bbe06..1507083 100644 --- a/docs/OFFLINE_MAPS_PLAN.md +++ b/docs/OFFLINE_MAPS_PLAN.md @@ -119,6 +119,15 @@ die Kacheln sind jederzeit neu ableitbarer Cache. offline nutzbar, auch nach „Alles löschen"/Eviction. Region-Dedupe per Typ+Name. - Stub-Tests jetzt im Repo: `tests/js/test-map-offline-r*.js` (s. tests/js/README.md). +**✅ Runde 6 — Standort-Grundversorgung (2026-06-08):** +Renés Original-Modell Punkt 1 („die App holt sich am Standort die Karten- und Markerdaten bis +5 MB") war zu eng als nur-Funkloch interpretiert: Das Gebiet um die AKTUELLE POSITION ist jetzt +IMMER geladen — `ensureHomeArea(lat, lon)` (Zentrums-Kachel-Check → bei Lücke Budget-Download, +type 'standort', Cap-gated). Greift: (a) im Start-Check (raw IDB-Check ohne GL-Stack-Load), +(b) SOFORT nach „Alles löschen" (Standort wird direkt neu geladen + Toast). Pfote Segment 5 = +Standort-Kachel da UND Zonen im 50-km-Umkreis gefüllt (ferne Zonen zählen nicht mehr — sie +laden erst vor Ort). Tests: tests/js/test-map-offline-r6.js. + **🔲 Offen (Backlog):** - Echte LRU-Eviction (Refcounting/Region-Zuordnung der Kacheln), wenn Nutzer real ans Cap kommen. - Rechteck-Zeichnen als präzisere Bereichsauswahl (Viewport-Variante deckt den Hauptfall ab). diff --git a/tests/js/README.md b/tests/js/README.md index e428ae5..c37328d 100644 --- a/tests/js/README.md +++ b/tests/js/README.md @@ -10,6 +10,7 @@ for f in tests/js/test-map-offline*.js; do node "$f" backend/static/js/map-offli - r3: downloadBbox, Zu-groß-Schutz, totalBytes, Prefetch-Throttle, Cap-Guard, persist() - r4: Minimal-Speicher-Modell (Prune, Netz-Probe, clear behält Zonen, Nähe/Verify, Färbung) - r5: Bbox-Replace (aufgehobene Warnungen), 24h-Alert-Refresh, removeDeadZone, ensureRouteCorridors +- r6: Standort-Grundversorgung (ensureHomeArea: lädt/skippt/Cap, überlebt clear) ⚠️ Node 21+: eingebautes `navigator`-Global — Stubs via `Object.defineProperty(globalThis, 'navigator', …)`, ein einfaches `global.navigator =` wird still verschluckt. diff --git a/tests/js/test-map-offline-r6.js b/tests/js/test-map-offline-r6.js new file mode 100644 index 0000000..b67b850 --- /dev/null +++ b/tests/js/test-map-offline-r6.js @@ -0,0 +1,68 @@ +// Runde-6-Tests: Standort-Grundversorgung (ensureHomeArea) +const fs = require('fs'); +const stores = { tiles: new Map(), meta: new Map() }; +function mkReq(result) { return { result }; } +global.indexedDB = { open() { + const req = {}; + setTimeout(() => { + const db = { + objectStoreNames: { contains: n => !!stores[n] }, + transaction(name) { + const os = { + get: k => mkReq(stores[name].get(k)), + put: (v, k) => { stores[name].set(k, v); return mkReq(undefined); }, + delete: k => { stores[name].delete(k); return mkReq(undefined); }, + clear: () => { stores[name].clear(); return mkReq(undefined); }, + count: () => mkReq(stores[name].size), + getAllKeys: () => mkReq([...stores[name].keys()]), + }; + const tx = { objectStore: () => os }; + setTimeout(() => tx.oncomplete && tx.oncomplete()); + return tx; + }, + close() {}, + }; + req.result = db; req.onsuccess && req.onsuccess(); + }); + return req; +} }; +global.window = {}; +Object.defineProperty(globalThis, 'navigator', { value: { onLine: true, storage: { persist: () => Promise.resolve(true) } }, configurable: true }); +global.pmtiles = { PMTiles: class { getZxy() { return Promise.resolve({ data: new Uint8Array(100).buffer }); } } }; +global.MapGLStyle = { tilesUrl: () => 'http://t/d.pmtiles' }; +global.fetch = () => Promise.resolve({ ok: true, arrayBuffer: () => Promise.resolve(new Uint8Array(50).buffer), json: () => Promise.resolve([]) }); +eval(fs.readFileSync(process.argv[2], 'utf8')); +const MO = global.window.MapOffline; + +(async () => { + // 1. Leerer Speicher → Standort wird geladen (type 'standort') + const r1 = await MO.ensureHomeArea(48.07, 11.96); + console.log('ensureHomeArea (leer):', r1, '— Kacheln:', stores.tiles.size); + if (r1 !== 1 || stores.tiles.size === 0) throw new Error('Grundversorgung lädt nicht'); + const reg = stores.meta.get('regions').find(r => r.type === 'standort'); + if (!reg) throw new Error('standort-Region fehlt'); + + // 2. Bestand vorhanden → kein Doppel-Download + const before = stores.tiles.size; + const r2 = await MO.ensureHomeArea(48.07, 11.96); + console.log('ensureHomeArea (vorhanden):', r2); + if (r2 !== 0 || stores.tiles.size !== before) throw new Error('Doppel-Download trotz Bestand'); + + // 3. clear() → Zonen bleiben, Standort weg → ensureHomeArea lädt neu + await MO.markDeadZone(48.07, 11.96); + await MO.clear(); + if (stores.tiles.size !== 0) throw new Error('clear unvollständig'); + const r3 = await MO.ensureHomeArea(48.07, 11.96); + console.log('Nach clear neu geladen:', r3, '— Zonen erhalten:', (stores.meta.get('deadzones') || []).length); + if (r3 !== 1) throw new Error('Reload nach clear fehlt'); + if ((stores.meta.get('deadzones') || []).length !== 1) throw new Error('Zonen weg'); + + // 4. Über Cap → Auto-Pfad lädt nicht + stores.meta.set('totalBytes', 300 * 1048576); + stores.tiles.clear(); + const r4 = await MO.ensureHomeArea(48.07, 11.96); + console.log('Über Cap:', r4); + if (r4 !== 0) throw new Error('Cap-Guard fehlt'); + + console.log('\nALLE RUNDE-6-TESTS BESTANDEN'); +})().catch(e => { console.error('FEHLER:', e.message); process.exit(1); }); From 29cd489287edaf5a81d4d3594c93ef0806180a12 Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 6 Jun 2026 13:35:18 +0200 Subject: [PATCH 2/4] =?UTF-8?q?Offline-Karten=20Runde=207:=20'Alles=20loes?= =?UTF-8?q?chen'=20selektiv=20=E2=80=94=20Standort=20+=20Routen-Korridore?= =?UTF-8?q?=20bleiben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Idee Rene (spart Vorladezeit + Daten): statt loeschen-und-neu-laden bleiben - Standort-Gebiete (Regionen type 'standort') - Korridore der gespeicherten Routen (clear({keepTracks}) aus preview_track) - 5-km-Umkreis der aktuellen Position + Basis-Zooms 0-9 - Marker/Warnungen (p/) + Glyphs (f/) Geloescht: manuelle Gebiete/Ausschnitte + Funkloch-Kacheln (Zonen bleiben gemerkt, Nahe laden automatisch neu). Ohne Keep-Kandidaten: Komplett-Wipe. Batch-Delete in einer Transaktion. Tests r7 neu, r6 angepasst, Regression gruen. Bump v1235 --- VERSION | 2 +- backend/static/index.html | 24 ++++---- backend/static/js/app.js | 2 +- backend/static/js/map-offline.js | 94 +++++++++++++++++++++++++++----- backend/static/js/pages/map.js | 19 +++++-- backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- docs/OFFLINE_MAPS_PLAN.md | 9 +++ tests/js/README.md | 1 + tests/js/test-map-offline-r6.js | 11 ++-- tests/js/test-map-offline-r7.js | 66 ++++++++++++++++++++++ 11 files changed, 193 insertions(+), 39 deletions(-) create mode 100644 tests/js/test-map-offline-r7.js diff --git a/VERSION b/VERSION index 274c005..8118744 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1234 \ No newline at end of file +1235 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index 9079a4c..cab567b 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -612,11 +612,11 @@ - - - - - + + + + + @@ -626,7 +626,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index c5890c0..04a0e05 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 = '1234'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1235'; // ← 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/map-offline.js b/backend/static/js/map-offline.js index 221fdfb..4324bda 100644 --- a/backend/static/js/map-offline.js +++ b/backend/static/js/map-offline.js @@ -39,6 +39,16 @@ window.MapOffline = (function () { var _put = function (k, v) { return _req(STORE, 'readwrite', function (os) { os.put(v, k); }); }; var _del = function (k) { return _req(STORE, 'readwrite', function (os) { os.delete(k); }); }; var _count = function () { return _req(STORE, 'readonly', function (os) { return os.count(); }); }; + // Viele Keys in EINER Transaktion löschen (einzelne _del-Transaktionen wären zu langsam). + function _delMany(keys) { + if (!keys.length) return Promise.resolve(); + return _open().then(function (d) { return new Promise(function (res, rej) { + var tx = d.transaction(STORE, 'readwrite'), os = tx.objectStore(STORE); + keys.forEach(function (k) { os.delete(k); }); + tx.oncomplete = function () { res(); }; + tx.onerror = function () { rej(tx.error); }; + }); }); + } 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); }); }; @@ -750,20 +760,78 @@ window.MapOffline = (function () { }); } function hasRegion() { return stats().then(function (s) { return s.count > 0; }).catch(function () { return false; }); } - // „Alles löschen" entfernt Kacheln/Marker/Regionen — das FUNKLOCH-GEDÄCHTNIS bleibt - // (Quelle der Wahrheit, Modell René 2026-06-08): Zonen werden auf filled:false gesetzt - // und beim nächsten Online-Start in Positionsnähe automatisch neu geladen. - function clear() { - return _req(STORE, 'readwrite', function (os) { os.clear(); }) - .then(function () { return _metaGet('deadzones'); }) - .then(function (zones) { - return _req(META, 'readwrite', function (os) { os.clear(); }).then(function () { - if (zones && zones.length) { - zones.forEach(function (z) { z.filled = false; }); - return _metaPut('deadzones', zones); - } - }); + // Kachel-Keys eines Umkreises (alle Zooms) ins Keep-Set legen. + function _keepRegionKeys(lat, lon, radiusKm, keep) { + var bb = _bboxAround(lat, lon, radiusKm); + for (var z = 10; z <= MAXZOOM; 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++) keep[z + '/' + x + '/' + y] = 1; + } + } + // Korridor-Keys eines Tracks (±bufferKm, z10–14) ins Keep-Set legen. + function _keepCorridorKeys(track, bufferKm, keep) { + track.forEach(function (p) { + var d = Math.ceil(bufferKm / _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++) { + keep[MAXZOOM + '/' + x + '/' + y] = 1; + for (var pz = 13; pz >= 10; pz--) { + keep[pz + '/' + (x >> (MAXZOOM - pz)) + '/' + (y >> (MAXZOOM - pz))] = 1; + } + } + }); + } + + // „Alles löschen" — SELEKTIV (René 2026-06-08, spart Vorladezeit): + // BLEIBEN: Standort-Gebiete (Regionen type 'standort'), Korridore der übergebenen + // Routen-Tracks (opts.keepTracks), der Umkreis von opts.center (5 km), Basis-Zooms 0–9 + // (winzig, von allem gebraucht), Marker/Warnungen ('p/') + Glyphs ('f/'). + // GEHEN: manuelle Gebiete/Ausschnitte + Funkloch-Kacheln (Zonen bleiben gemerkt, + // filled:false → Start-Check lädt Nahe automatisch neu). + // Ohne Keep-Kandidaten (alte Signatur/Tests): kompletter Wipe inkl. Basis-Zooms. + function clear(opts) { + opts = opts || {}; + var keep = {}; + return _metaGet('regions').then(function (regions) { + regions = regions || []; + var keptRegions = regions.filter(function (r) { return r.type === 'standort' || r.type === 'korridor'; }); + regions.forEach(function (r) { + if (r.type === 'standort' && r.radiusKm) _keepRegionKeys(r.lat, r.lon, r.radiusKm, keep); }); + if (opts.center) _keepRegionKeys(opts.center.lat, opts.center.lon, 5, keep); + (opts.keepTracks || []).forEach(function (t) { + if (t && t.length >= 2) _keepCorridorKeys(t, 1, keep); + }); + var keepBase = Object.keys(keep).length > 0; + if (!keepBase) keptRegions = []; // nichts zu behalten → echter Komplett-Wipe + + return _req(STORE, 'readonly', function (os) { return os.getAllKeys(); }).then(function (keys) { + // Komplett-Wipe (nichts zu behalten): alles inkl. Marker/Glyphs (altes Verhalten). + if (!keepBase) return _delMany((keys || []).slice()); + var doomed = (keys || []).filter(function (k) { + var m = /^(\d+)\//.exec(k); + if (!m) return false; // 'p/' + 'f/' bleiben + if (+m[1] <= 9) return false; // Basis-Zooms behalten + return !keep[k]; + }); + return _delMany(doomed); + }).then(function () { return _metaGet('deadzones'); }) + .then(function (zones) { + return _req(META, 'readwrite', function (os) { os.clear(); }).then(function () { + var jobs = []; + if (zones && zones.length) { + zones.forEach(function (z) { z.filled = false; }); + jobs.push(_metaPut('deadzones', zones)); + } + if (keptRegions.length) { + jobs.push(_metaPut('regions', keptRegions)); + jobs.push(_metaPut('region', keptRegions[keptRegions.length - 1])); + jobs.push(_metaPut('totalBytes', keptRegions.reduce(function (a, r) { return a + (r.bytes || 0); }, 0))); + } + return Promise.all(jobs); + }); + }); + }); } return { diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index 7569458..dc25e0d 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -2308,13 +2308,22 @@ window.Page_map = (() => { btn.innerHTML = `${UI.icon('trash')} Wirklich alles löschen?`; return; } - await MapOffline.clear().catch(() => {}); + // SELEKTIV löschen (René 2026-06-08, spart Vorladezeit): Standort-Gebiet + Korridore + // der gespeicherten Routen bleiben einfach stehen statt löschen-und-neu-laden. + let keepTracks = []; + try { + keepTracks = ((await API.routes.list()) || []) + .map(r => r.preview_track).filter(t => (t || []).length >= 2); + } catch (e) {} + await MapOffline.clear({ + center: _userPos ? { lat: _userPos.lat, lon: _userPos.lon } : null, + keepTracks, + }).catch(() => {}); _setCoverage(false); UI.modal.close(); - UI.toast.success('Offline-Karten gelöscht. Funkloch-Gebiete werden beim nächsten Start automatisch neu geladen.'); - // Standort-Grundversorgung sofort wiederherstellen (René 2026-06-08: das Gebiet am - // aktuellen Standort muss bleiben — es würde sonst nicht automatisch vorgeladen - // und die Offline-Funktionalität wäre genau hier weg). + UI.toast.success('Offline-Karten gelöscht — Standort-Gebiet und Routen-Korridore bleiben erhalten.'); + // Sicherheitsnetz: falls am Standort nichts zu behalten war (z.B. nie geladen), + // Grundversorgung jetzt herstellen. if (_userPos && navigator.onLine) { try { const r = await MapOffline.ensureHomeArea(_userPos.lat, _userPos.lon); diff --git a/backend/static/landing.html b/backend/static/landing.html index 2266243..dbf53df 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 cb819ee..c9ef6c8 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 = '1234'; +const VER = '1235'; 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/docs/OFFLINE_MAPS_PLAN.md b/docs/OFFLINE_MAPS_PLAN.md index 1507083..274539b 100644 --- a/docs/OFFLINE_MAPS_PLAN.md +++ b/docs/OFFLINE_MAPS_PLAN.md @@ -128,6 +128,15 @@ type 'standort', Cap-gated). Greift: (a) im Start-Check (raw IDB-Check ohne GL-S Standort-Kachel da UND Zonen im 50-km-Umkreis gefüllt (ferne Zonen zählen nicht mehr — sie laden erst vor Ort). Tests: tests/js/test-map-offline-r6.js. +**✅ Runde 7 — selektives Löschen (2026-06-08, Idee René: Vorladezeit sparen):** +„Alles löschen" löscht nicht mehr alles-und-lädt-neu, sondern **behält** Standort-Gebiete +(`type 'standort'` aus der Regions-Meta), die **Korridore der gespeicherten Routen** +(`clear({keepTracks})`, Tracks via API.routes.list/preview_track), den 5-km-Umkreis der +aktuellen Position, Basis-Zooms 0–9 sowie Marker/Warnungen + Glyphs. Gelöscht werden manuelle +Gebiete/Ausschnitte + Funkloch-Kacheln (Zonen bleiben gemerkt → Nahe laden automatisch neu). +Ohne Keep-Kandidaten: Komplett-Wipe wie bisher. Batch-Delete in einer Transaktion. +Tests: r7 (+ r6 angepasst). + **🔲 Offen (Backlog):** - Echte LRU-Eviction (Refcounting/Region-Zuordnung der Kacheln), wenn Nutzer real ans Cap kommen. - Rechteck-Zeichnen als präzisere Bereichsauswahl (Viewport-Variante deckt den Hauptfall ab). diff --git a/tests/js/README.md b/tests/js/README.md index c37328d..1b5675e 100644 --- a/tests/js/README.md +++ b/tests/js/README.md @@ -11,6 +11,7 @@ for f in tests/js/test-map-offline*.js; do node "$f" backend/static/js/map-offli - r4: Minimal-Speicher-Modell (Prune, Netz-Probe, clear behält Zonen, Nähe/Verify, Färbung) - r5: Bbox-Replace (aufgehobene Warnungen), 24h-Alert-Refresh, removeDeadZone, ensureRouteCorridors - r6: Standort-Grundversorgung (ensureHomeArea: lädt/skippt/Cap, überlebt clear) +- r7: selektives Löschen (Korridor-Keep via keepTracks, manuelle Gebiete weg, Komplett-Wipe-Fallback) ⚠️ Node 21+: eingebautes `navigator`-Global — Stubs via `Object.defineProperty(globalThis, 'navigator', …)`, ein einfaches `global.navigator =` wird still verschluckt. diff --git a/tests/js/test-map-offline-r6.js b/tests/js/test-map-offline-r6.js index b67b850..cfbf1d9 100644 --- a/tests/js/test-map-offline-r6.js +++ b/tests/js/test-map-offline-r6.js @@ -48,14 +48,15 @@ const MO = global.window.MapOffline; console.log('ensureHomeArea (vorhanden):', r2); if (r2 !== 0 || stores.tiles.size !== before) throw new Error('Doppel-Download trotz Bestand'); - // 3. clear() → Zonen bleiben, Standort weg → ensureHomeArea lädt neu + // 3. clear() SELEKTIV: standort-Region bleibt stehen, Zonen bleiben gemerkt await MO.markDeadZone(48.07, 11.96); + const beforeClear = stores.tiles.size; await MO.clear(); - if (stores.tiles.size !== 0) throw new Error('clear unvollständig'); - const r3 = await MO.ensureHomeArea(48.07, 11.96); - console.log('Nach clear neu geladen:', r3, '— Zonen erhalten:', (stores.meta.get('deadzones') || []).length); - if (r3 !== 1) throw new Error('Reload nach clear fehlt'); + console.log('Nach clear: tiles', beforeClear, '→', stores.tiles.size, '— Zonen:', (stores.meta.get('deadzones') || []).length); + if (stores.tiles.size === 0) throw new Error('Standort-Gebiet überlebte clear nicht'); if ((stores.meta.get('deadzones') || []).length !== 1) throw new Error('Zonen weg'); + const r3 = await MO.ensureHomeArea(48.07, 11.96); + if (r3 !== 0) throw new Error('Standort hätte NICHT neu geladen werden müssen'); // 4. Über Cap → Auto-Pfad lädt nicht stores.meta.set('totalBytes', 300 * 1048576); diff --git a/tests/js/test-map-offline-r7.js b/tests/js/test-map-offline-r7.js new file mode 100644 index 0000000..20d64a6 --- /dev/null +++ b/tests/js/test-map-offline-r7.js @@ -0,0 +1,66 @@ +// Runde-7-Tests: selektives Löschen (Korridor-Keep, manuelles Gebiet weg, Komplett-Wipe) +const fs = require('fs'); +const stores = { tiles: new Map(), meta: new Map() }; +function mkReq(result) { return { result }; } +global.indexedDB = { open() { + const req = {}; + setTimeout(() => { + const db = { + objectStoreNames: { contains: n => !!stores[n] }, + transaction(name) { + const os = { + get: k => mkReq(stores[name].get(k)), + put: (v, k) => { stores[name].set(k, v); return mkReq(undefined); }, + delete: k => { stores[name].delete(k); return mkReq(undefined); }, + clear: () => { stores[name].clear(); return mkReq(undefined); }, + count: () => mkReq(stores[name].size), + getAllKeys: () => mkReq([...stores[name].keys()]), + }; + const tx = { objectStore: () => os }; + setTimeout(() => tx.oncomplete && tx.oncomplete()); + return tx; + }, + close() {}, + }; + req.result = db; req.onsuccess && req.onsuccess(); + }); + return req; +} }; +global.window = {}; +Object.defineProperty(globalThis, 'navigator', { value: { onLine: true, storage: { persist: () => Promise.resolve(true) } }, configurable: true }); +global.pmtiles = { PMTiles: class { getZxy() { return Promise.resolve({ data: new Uint8Array(100).buffer }); } } }; +global.MapGLStyle = { tilesUrl: () => 'http://t/d.pmtiles' }; +global.fetch = () => Promise.resolve({ ok: true, arrayBuffer: () => Promise.resolve(new Uint8Array(50).buffer), json: () => Promise.resolve([{ id: 1, lat: 48.2, lon: 12.1 }]) }); +eval(fs.readFileSync(process.argv[2], 'utf8')); +const MO = global.window.MapOffline; + +(async () => { + // Setup: manuelles Gebiet weit weg (lon 8) + Routen-Korridor (lon 12.1) + await MO.downloadAround(50.0, 8.0, { budgetMB: 0.005 }); + const track = [{ lat: 48.20, lon: 12.10 }, { lat: 48.21, lon: 12.12 }, { lat: 48.22, lon: 12.14 }]; + await MO.downloadCorridor(track, { bufferKm: 1, name: 'R' }); + const before = stores.tiles.size; + const poisBefore = [...stores.tiles.keys()].filter(k => k.startsWith('p/')).length; + + // 1. Selektiv: Korridor bleibt, manuelles Gebiet verschwindet, Marker bleiben + await MO.clear({ keepTracks: [track] }); + const gj = await MO.coverage(); + const lons = gj.features.map(f => f.geometry.coordinates[0][0][0]); + console.log('Selektiv: tiles', before, '→', stores.tiles.size, '— Coverage-Features:', gj.features.length); + if (!gj.features.length) throw new Error('Korridor überlebte nicht'); + if (lons.some(l => l < 11.5)) throw new Error('Manuelles Gebiet (lon 8) überlebte'); + const poisAfter = [...stores.tiles.keys()].filter(k => k.startsWith('p/')).length; + if (poisAfter !== poisBefore) throw new Error('Marker-Stores überlebten nicht'); + const regs = stores.meta.get('regions') || []; + if (!regs.length || regs.some(r => r.type === 'gebiet')) throw new Error('Regions-Meta falsch gefiltert'); + + // 2. Komplett-Wipe ohne Keep-Kandidaten: alles weg (auch p/ + f/), Zonen bleiben + await MO.markDeadZone(48.2, 12.1); + await MO.clear(); + console.log('Komplett-Wipe: tiles =', stores.tiles.size, '— Zonen:', (stores.meta.get('deadzones') || []).length); + // Korridor-Region hat keinen Track in der Meta → kein Keep-Set → echter Wipe + if (stores.tiles.size !== 0) throw new Error('Komplett-Wipe unvollständig'); + if ((stores.meta.get('deadzones') || []).length !== 1) throw new Error('Zonen weg'); + + console.log('\nALLE RUNDE-7-TESTS BESTANDEN'); +})().catch(e => { console.error('FEHLER:', e.message); process.exit(1); }); From ca976139384153c6ca370b837235bea129210e1c Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 6 Jun 2026 13:44:01 +0200 Subject: [PATCH 3/4] =?UTF-8?q?Fix:=20Selektives=20Loeschen=20griff=20auf?= =?UTF-8?q?=20Geraet=20nicht=20=E2=80=94=20Keep-Set=20jetzt=20selbsttragen?= =?UTF-8?q?d=20aus=20der=20Meta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Renes Befund: 'Alles loeschen' wischte weiter alles. Ursachen: (a) Bestands- Gebiete hatten keine standort-Region (ensureHomeArea legt nur bei FEHLENDER Kachel los), (b) Korridor-Keys waren nur aus API-Tracks ableitbar -> leeres Keep-Set = Komplett-Wipe. - downloadCorridor speichert vereinfachten Track (<=60 Pkt) in der Region-Meta; clear() baut Korridor-Keep daraus — ohne API/Login/GPS - Standort-ADOPTION: clear() mit center legt fehlende standort-Region synthetisch an (Bestandsdaten vor Runde 6) - map.js: center-Fallback auf by_last_position wenn GPS noch keinen Fix hat - Test r7 erweitert (clear ohne Optionen haelt Korridor aus Meta), alle gruen Bump v1236 --- VERSION | 2 +- backend/static/index.html | 24 ++++++++++++------------ backend/static/js/app.js | 2 +- backend/static/js/map-offline.js | 22 ++++++++++++++++++++-- backend/static/js/pages/map.js | 14 ++++++++++---- backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- tests/js/test-map-offline-r7.js | 12 ++++++++++-- 8 files changed, 56 insertions(+), 24 deletions(-) diff --git a/VERSION b/VERSION index 8118744..125a103 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1235 \ No newline at end of file +1236 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index cab567b..b4cd336 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -612,11 +612,11 @@ - - - - - + + + + + @@ -626,7 +626,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 04a0e05..512e11d 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 = '1235'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1236'; // ← 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/map-offline.js b/backend/static/js/map-offline.js index 4324bda..d743721 100644 --- a/backend/static/js/map-offline.js +++ b/backend/static/js/map-offline.js @@ -598,13 +598,21 @@ window.MapOffline = (function () { if (opts.onProgress) opts.onProgress({ bytes: state.bytes, done: state.done, total: total }); }).then(function () { return chunkLoop(idx + 64); }); } + // Vereinfachten Track (≤60 Punkte) in der Region-Meta ablegen: damit kann clear() + // die Korridor-Keep-Keys SELBST aus IndexedDB bauen — ohne API/Login/GPS + // (Gerätetest René 2026-06-08: leeres Keep-Set = Komplett-Wipe trotz Routen). + var step = Math.max(1, Math.ceil(track.length / 60)); + var slim = track.filter(function (p, i) { return i % step === 0; }); + if (slim[slim.length - 1] !== track[track.length - 1]) slim.push(track[track.length - 1]); + slim = slim.map(function (p) { return { lat: p.lat, lon: p.lon }; }); + 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() }); + track: slim, tiles: state.stored, bytes: state.bytes, pois: poiCount, savedAt: Date.now() }); }) .then(function () { return _bumpTotal(state.bytes); }) .then(function () { return { tiles: state.stored, bytes: state.bytes, pois: poiCount, capped: state.bytes >= cap }; }); @@ -797,8 +805,18 @@ window.MapOffline = (function () { var keptRegions = regions.filter(function (r) { return r.type === 'standort' || r.type === 'korridor'; }); regions.forEach(function (r) { if (r.type === 'standort' && r.radiusKm) _keepRegionKeys(r.lat, r.lon, r.radiusKm, keep); + // Korridor-Keep direkt aus der Region-Meta (r.track) — unabhängig von API/Login. + if (r.type === 'korridor' && r.track && r.track.length >= 2) _keepCorridorKeys(r.track, 1, keep); }); - if (opts.center) _keepRegionKeys(opts.center.lat, opts.center.lon, 5, keep); + if (opts.center) { + _keepRegionKeys(opts.center.lat, opts.center.lon, 5, keep); + // Standort-ADOPTION: Bestandsdaten haben evtl. keine standort-Region (Gebiet wurde + // vor Runde 6 geladen) — Eintrag synthetisch anlegen, damit Folge-Läufe ihn kennen. + if (!keptRegions.some(function (r) { return r.type === 'standort'; })) { + keptRegions.push({ type: 'standort', lat: opts.center.lat, lon: opts.center.lon, + radiusKm: 5, tiles: 0, bytes: 0, pois: 0, savedAt: Date.now() }); + } + } (opts.keepTracks || []).forEach(function (t) { if (t && t.length >= 2) _keepCorridorKeys(t, 1, keep); }); diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index dc25e0d..9adc8e0 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -2310,15 +2310,21 @@ window.Page_map = (() => { } // SELEKTIV löschen (René 2026-06-08, spart Vorladezeit): Standort-Gebiet + Korridore // der gespeicherten Routen bleiben einfach stehen statt löschen-und-neu-laden. + // (Korridor-Keep kommt primär aus der Region-Meta; API-Tracks sind Ergänzung.) let keepTracks = []; try { keepTracks = ((await API.routes.list()) || []) .map(r => r.preview_track).filter(t => (t || []).length >= 2); } catch (e) {} - await MapOffline.clear({ - center: _userPos ? { lat: _userPos.lat, lon: _userPos.lon } : null, - keepTracks, - }).catch(() => {}); + // Position: GPS-Fix, sonst letzte bekannte Position (wetter.js et al.) + let center = _userPos ? { lat: _userPos.lat, lon: _userPos.lon } : null; + if (!center) { + try { + const p = JSON.parse(localStorage.getItem('by_last_position') || 'null'); + if (p?.lat != null) center = { lat: p.lat, lon: p.lon }; + } catch (e) {} + } + await MapOffline.clear({ center, keepTracks }).catch(() => {}); _setCoverage(false); UI.modal.close(); UI.toast.success('Offline-Karten gelöscht — Standort-Gebiet und Routen-Korridore bleiben erhalten.'); diff --git a/backend/static/landing.html b/backend/static/landing.html index dbf53df..7e2b317 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 c9ef6c8..b9d41dc 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 = '1235'; +const VER = '1236'; 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/tests/js/test-map-offline-r7.js b/tests/js/test-map-offline-r7.js index 20d64a6..75c4ee1 100644 --- a/tests/js/test-map-offline-r7.js +++ b/tests/js/test-map-offline-r7.js @@ -54,11 +54,19 @@ const MO = global.window.MapOffline; const regs = stores.meta.get('regions') || []; if (!regs.length || regs.some(r => r.type === 'gebiet')) throw new Error('Regions-Meta falsch gefiltert'); - // 2. Komplett-Wipe ohne Keep-Kandidaten: alles weg (auch p/ + f/), Zonen bleiben + // 2. clear() OHNE keepTracks: Korridor-Keep kommt aus der Region-Meta (r.track) + const afterSel = stores.tiles.size; + await MO.clear(); + console.log('clear ohne Optionen: tiles', afterSel, '→', stores.tiles.size, '(Korridor aus Meta gehalten)'); + if (stores.tiles.size === 0) throw new Error('Korridor-Keep aus Region-Meta fehlt'); + const gj2 = await MO.coverage(); + if (!gj2.features.length) throw new Error('Korridor-Coverage leer'); + + // 3. Komplett-Wipe: Regionen-Meta manuell leeren → kein Keep-Kandidat → alles weg await MO.markDeadZone(48.2, 12.1); + stores.meta.delete('regions'); await MO.clear(); console.log('Komplett-Wipe: tiles =', stores.tiles.size, '— Zonen:', (stores.meta.get('deadzones') || []).length); - // Korridor-Region hat keinen Track in der Meta → kein Keep-Set → echter Wipe if (stores.tiles.size !== 0) throw new Error('Komplett-Wipe unvollständig'); if ((stores.meta.get('deadzones') || []).length !== 1) throw new Error('Zonen weg'); From 2cdb743ce7750af3806d9a06a3e0762ad3764ab4 Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 6 Jun 2026 13:55:37 +0200 Subject: [PATCH 4/4] Selektives Loeschen: auch Funkloch-Gebiete bleiben + Keep-Set haertung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rene: Funkloecher + Routen waren nach 'Alles loeschen' weiter weg. - Funkloch-Regionen jetzt im Keep-Set (geloescht wird NUR Manuelles); Zonen behalten ihren Fuellstatus (Komplett-Wipe setzt weiter zurueck) - Korridor-Migration beim Loeschen: keepTracks=[{name,track}] schreibt Tracks in Alt-Eintraege ohne r.track (Bestand vor v1236) bzw. legt fehlende Korridor-Regionen an — kein Warten auf Self-Healing - clear() liefert Summary; Toast zeigt 'behalten: Standort, X Routen, Y Funkloch-Gebiete' — Diagnose-Sichtbarkeit fuer Geraetetests Bump v1237 --- VERSION | 2 +- backend/static/index.html | 24 ++++++------- backend/static/js/app.js | 2 +- backend/static/js/map-offline.js | 62 ++++++++++++++++++++++++-------- backend/static/js/pages/map.js | 14 ++++++-- backend/static/landing.html | 2 +- backend/static/sw.js | 2 +- docs/OFFLINE_MAPS_PLAN.md | 9 +++-- tests/js/test-map-offline-r7.js | 15 +++++++- 9 files changed, 94 insertions(+), 38 deletions(-) diff --git a/VERSION b/VERSION index 125a103..96a2d52 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1236 \ No newline at end of file +1237 \ No newline at end of file diff --git a/backend/static/index.html b/backend/static/index.html index b4cd336..fb7fa45 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -86,14 +86,14 @@ Ban Yaro - + - - - - - + + + + + @@ -612,11 +612,11 @@ - - - - - + + + + + @@ -626,7 +626,7 @@ - + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 512e11d..37a62d1 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 = '1236'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '1237'; // ← 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/map-offline.js b/backend/static/js/map-offline.js index d743721..e16053f 100644 --- a/backend/static/js/map-offline.js +++ b/backend/static/js/map-offline.js @@ -791,22 +791,54 @@ window.MapOffline = (function () { } // „Alles löschen" — SELEKTIV (René 2026-06-08, spart Vorladezeit): - // BLEIBEN: Standort-Gebiete (Regionen type 'standort'), Korridore der übergebenen - // Routen-Tracks (opts.keepTracks), der Umkreis von opts.center (5 km), Basis-Zooms 0–9 - // (winzig, von allem gebraucht), Marker/Warnungen ('p/') + Glyphs ('f/'). - // GEHEN: manuelle Gebiete/Ausschnitte + Funkloch-Kacheln (Zonen bleiben gemerkt, - // filled:false → Start-Check lädt Nahe automatisch neu). + // BLEIBEN: Standort-Gebiete, FUNKLOCH-Gebiete (automatisch gelernt — wären sonst sofort + // wieder vorzuladen), Korridore der Routen (aus Region-Meta r.track ODER opts.keepTracks), + // der Umkreis von opts.center (5 km), Basis-Zooms 0–9, Marker/Warnungen ('p/') + Glyphs ('f/'). + // GEHEN: nur manuelle Gebiete ('gebiet') + Ausschnitte ('ausschnitt'). + // opts.keepTracks: [{name, track}] oder [track] — Tracks werden in die Korridor-Meta + // MIGRIERT (Bestandsdaten vor v1236 hatten keinen track im Eintrag). // Ohne Keep-Kandidaten (alte Signatur/Tests): kompletter Wipe inkl. Basis-Zooms. + // Liefert {standort, funkloch, korridore} (Anzahl behaltener Gebiete) für den Toast. function clear(opts) { opts = opts || {}; - var keep = {}; + var keep = {}, summary = { standort: 0, funkloch: 0, korridore: 0 }; return _metaGet('regions').then(function (regions) { regions = regions || []; - var keptRegions = regions.filter(function (r) { return r.type === 'standort' || r.type === 'korridor'; }); - regions.forEach(function (r) { - if (r.type === 'standort' && r.radiusKm) _keepRegionKeys(r.lat, r.lon, r.radiusKm, keep); + var keptRegions = regions.filter(function (r) { + return r.type === 'standort' || r.type === 'korridor' || r.type === 'funkloch'; + }); + // Tracks normalisieren: [{name, track}] oder [track] + var tracks = (opts.keepTracks || []).map(function (t) { + return Array.isArray(t) ? { name: null, track: t } : (t || {}); + }).filter(function (o) { return o.track && o.track.length >= 2; }); + + keptRegions.forEach(function (r) { + if ((r.type === 'standort' || r.type === 'funkloch') && r.radiusKm) { + _keepRegionKeys(r.lat, r.lon, r.radiusKm, keep); + summary[r.type]++; + } // Korridor-Keep direkt aus der Region-Meta (r.track) — unabhängig von API/Login. - if (r.type === 'korridor' && r.track && r.track.length >= 2) _keepCorridorKeys(r.track, 1, keep); + if (r.type === 'korridor') { + if (!(r.track && r.track.length >= 2)) { + // Migration: Track aus keepTracks in den Alt-Eintrag übernehmen (Match per Name, + // sonst erster track-loser Kandidat). + var m = tracks.find(function (o) { return o.name && o.name === r.name; }) || tracks[0]; + if (m) { r.track = m.track; } + } + if (r.track && r.track.length >= 2) { _keepCorridorKeys(r.track, 1, keep); summary.korridore++; } + } + }); + // keepTracks ohne passenden Meta-Eintrag → trotzdem behalten + Eintrag anlegen. + tracks.forEach(function (o) { + var known = keptRegions.some(function (r) { + return r.type === 'korridor' && r.track && r.track.length && + _distKm(r.track[0].lat, r.track[0].lon, o.track[0].lat, o.track[0].lon) < 0.3; + }); + if (known) return; + _keepCorridorKeys(o.track, 1, keep); + summary.korridore++; + keptRegions.push({ type: 'korridor', name: o.name || null, lat: o.track[0].lat, lon: o.track[0].lon, + track: o.track, tiles: 0, bytes: 0, pois: 0, savedAt: Date.now() }); }); if (opts.center) { _keepRegionKeys(opts.center.lat, opts.center.lon, 5, keep); @@ -816,10 +848,8 @@ window.MapOffline = (function () { keptRegions.push({ type: 'standort', lat: opts.center.lat, lon: opts.center.lon, radiusKm: 5, tiles: 0, bytes: 0, pois: 0, savedAt: Date.now() }); } + summary.standort = Math.max(summary.standort, 1); } - (opts.keepTracks || []).forEach(function (t) { - if (t && t.length >= 2) _keepCorridorKeys(t, 1, keep); - }); var keepBase = Object.keys(keep).length > 0; if (!keepBase) keptRegions = []; // nichts zu behalten → echter Komplett-Wipe @@ -838,7 +868,9 @@ window.MapOffline = (function () { return _req(META, 'readwrite', function (os) { os.clear(); }).then(function () { var jobs = []; if (zones && zones.length) { - zones.forEach(function (z) { z.filled = false; }); + // Selektiv: Funkloch-Kacheln bleiben → Füllstatus behalten (Start-Check + // verifiziert ohnehin). Komplett-Wipe: alles ungefüllt → Neu-Laden. + if (!keepBase) zones.forEach(function (z) { z.filled = false; }); jobs.push(_metaPut('deadzones', zones)); } if (keptRegions.length) { @@ -848,7 +880,7 @@ window.MapOffline = (function () { } return Promise.all(jobs); }); - }); + }).then(function () { return summary; }); }); } diff --git a/backend/static/js/pages/map.js b/backend/static/js/pages/map.js index 9adc8e0..6fd35bb 100644 --- a/backend/static/js/pages/map.js +++ b/backend/static/js/pages/map.js @@ -2314,7 +2314,8 @@ window.Page_map = (() => { let keepTracks = []; try { keepTracks = ((await API.routes.list()) || []) - .map(r => r.preview_track).filter(t => (t || []).length >= 2); + .map(r => ({ name: r.name, track: r.preview_track })) + .filter(o => (o.track || []).length >= 2); } catch (e) {} // Position: GPS-Fix, sonst letzte bekannte Position (wetter.js et al.) let center = _userPos ? { lat: _userPos.lat, lon: _userPos.lon } : null; @@ -2324,10 +2325,17 @@ window.Page_map = (() => { if (p?.lat != null) center = { lat: p.lat, lon: p.lon }; } catch (e) {} } - await MapOffline.clear({ center, keepTracks }).catch(() => {}); + const sum = await MapOffline.clear({ center, keepTracks }).catch(() => null); _setCoverage(false); UI.modal.close(); - UI.toast.success('Offline-Karten gelöscht — Standort-Gebiet und Routen-Korridore bleiben erhalten.'); + // Sichtbarkeit, WAS behalten wurde — Diagnose-Hilfe für Gerätetests. + const kept = []; + if (sum?.standort) kept.push('Standort'); + if (sum?.korridore) kept.push(`${sum.korridore} Route${sum.korridore === 1 ? '' : 'n'}`); + if (sum?.funkloch) kept.push(`${sum.funkloch} Funkloch-Gebiet${sum.funkloch === 1 ? '' : 'e'}`); + UI.toast.success(kept.length + ? `Manuelle Gebiete gelöscht — behalten: ${kept.join(', ')}.` + : 'Offline-Karten gelöscht.'); // Sicherheitsnetz: falls am Standort nichts zu behalten war (z.B. nie geladen), // Grundversorgung jetzt herstellen. if (_userPos && navigator.onLine) { diff --git a/backend/static/landing.html b/backend/static/landing.html index 7e2b317..956917e 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 b9d41dc..c3fa9b5 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 = '1236'; +const VER = '1237'; 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/docs/OFFLINE_MAPS_PLAN.md b/docs/OFFLINE_MAPS_PLAN.md index 274539b..924d927 100644 --- a/docs/OFFLINE_MAPS_PLAN.md +++ b/docs/OFFLINE_MAPS_PLAN.md @@ -132,9 +132,12 @@ laden erst vor Ort). Tests: tests/js/test-map-offline-r6.js. „Alles löschen" löscht nicht mehr alles-und-lädt-neu, sondern **behält** Standort-Gebiete (`type 'standort'` aus der Regions-Meta), die **Korridore der gespeicherten Routen** (`clear({keepTracks})`, Tracks via API.routes.list/preview_track), den 5-km-Umkreis der -aktuellen Position, Basis-Zooms 0–9 sowie Marker/Warnungen + Glyphs. Gelöscht werden manuelle -Gebiete/Ausschnitte + Funkloch-Kacheln (Zonen bleiben gemerkt → Nahe laden automatisch neu). -Ohne Keep-Kandidaten: Komplett-Wipe wie bisher. Batch-Delete in einer Transaktion. +aktuellen Position, Basis-Zooms 0–9 sowie Marker/Warnungen + Glyphs. Seit v1237 bleiben auch FUNKLOCH-Gebiete +(René: sonst sofort wieder Vorladezeit) — gelöscht wird NUR Manuelles ('gebiet'/'ausschnitt'). +Keep-Set ist SELBSTTRAGEND aus der Region-Meta (Korridor-Track ≤60 Pkt im Eintrag, r.track; +Standort-Adoption; keepTracks=[{name,track}] migriert Alt-Einträge). clear() liefert Summary +{standort, funkloch, korridore} → Toast zeigt, was behalten wurde (Diagnose). +Ohne Keep-Kandidaten: Komplett-Wipe (Zonen → ungefüllt). Batch-Delete in einer Transaktion. Tests: r7 (+ r6 angepasst). **🔲 Offen (Backlog):** diff --git a/tests/js/test-map-offline-r7.js b/tests/js/test-map-offline-r7.js index 75c4ee1..70e6dd6 100644 --- a/tests/js/test-map-offline-r7.js +++ b/tests/js/test-map-offline-r7.js @@ -54,6 +54,17 @@ const MO = global.window.MapOffline; const regs = stores.meta.get('regions') || []; if (!regs.length || regs.some(r => r.type === 'gebiet')) throw new Error('Regions-Meta falsch gefiltert'); + // 1b. Funkloch-Gebiet bleibt ebenfalls (René 2026-06-08: nur Manuelles löschen) + await MO.downloadAround(48.5, 12.5, { budgetMB: 0.005, type: 'funkloch' }); + await MO.markDeadZone(48.5, 12.5); + stores.meta.get('deadzones').forEach(z => z.filled = true); + const s1b = await MO.clear(); + console.log('Funkloch-Keep:', JSON.stringify(s1b), '— Zone gefüllt geblieben:', stores.meta.get('deadzones')[0].filled === true); + if (!s1b.funkloch) throw new Error('Funkloch-Gebiet überlebte nicht'); + if (stores.meta.get('deadzones')[0].filled !== true) throw new Error('Zonen-Status fälschlich zurückgesetzt'); + const fzKeys = [...stores.tiles.keys()].filter(k => /^1[0-4]\//.test(k)).length; + if (!fzKeys) throw new Error('Funkloch-Kacheln weg'); + // 2. clear() OHNE keepTracks: Korridor-Keep kommt aus der Region-Meta (r.track) const afterSel = stores.tiles.size; await MO.clear(); @@ -68,7 +79,9 @@ const MO = global.window.MapOffline; await MO.clear(); console.log('Komplett-Wipe: tiles =', stores.tiles.size, '— Zonen:', (stores.meta.get('deadzones') || []).length); if (stores.tiles.size !== 0) throw new Error('Komplett-Wipe unvollständig'); - if ((stores.meta.get('deadzones') || []).length !== 1) throw new Error('Zonen weg'); + const wipeZones = stores.meta.get('deadzones') || []; + if (!wipeZones.length) throw new Error('Zonen weg'); + if (wipeZones.some(z => z.filled)) throw new Error('Komplett-Wipe muss Zonen auf ungefüllt setzen'); console.log('\nALLE RUNDE-7-TESTS BESTANDEN'); })().catch(e => { console.error('FEHLER:', e.message); process.exit(1); });