Compare commits

...

4 commits

Author SHA1 Message Date
2cdb743ce7 Selektives Loeschen: auch Funkloch-Gebiete bleiben + Keep-Set haertung
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
2026-06-06 13:55:37 +02:00
ca97613938 Fix: Selektives Loeschen griff auf Geraet nicht — Keep-Set jetzt selbsttragend aus der Meta
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
2026-06-06 13:44:01 +02:00
29cd489287 Offline-Karten Runde 7: 'Alles loeschen' selektiv — Standort + Routen-Korridore bleiben
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
2026-06-06 13:35:18 +02:00
94a6ce49ba Offline-Karten Runde 6: Standort-Grundversorgung — aktuelles Gebiet bleibt immer geladen
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
2026-06-06 13:23:33 +02:00
12 changed files with 442 additions and 41 deletions

View file

@ -1 +1 @@
1233
1237

View file

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

View file

@ -3,7 +3,7 @@
Router, State-Management, Navigation, Initialisierung.
============================================================ */
const APP_VER = '1233'; // ← 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;

View file

@ -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); }); };
@ -588,18 +598,41 @@ 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 }; });
}
// 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
@ -735,25 +768,125 @@ 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, z1014) 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, 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 09, 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 = {}, 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' || 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') {
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);
// 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() });
}
summary.standort = Math.max(summary.standort, 1);
}
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) {
// 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) {
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);
});
}).then(function () { return summary; });
});
}
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,

View file

@ -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.`);

View file

@ -2308,10 +2308,42 @@ 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.
// (Korridor-Keep kommt primär aus der Region-Meta; API-Tracks sind Ergänzung.)
let keepTracks = [];
try {
keepTracks = ((await API.routes.list()) || [])
.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;
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) {}
}
const sum = await MapOffline.clear({ center, keepTracks }).catch(() => null);
_setCoverage(false);
UI.modal.close();
UI.toast.success('Offline-Karten gelöscht. Bekannte Funkloch-Gebiete werden beim nächsten Start automatisch neu geladen.');
// 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) {
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();
});
}

View file

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

View file

@ -4,7 +4,7 @@
============================================================ */
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
const VER = '1233';
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

View file

@ -119,6 +119,27 @@ 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.
**✅ 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 09 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):**
- 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).

View file

@ -10,6 +10,8 @@ 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)
- 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.

View file

@ -0,0 +1,69 @@
// 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() SELEKTIV: standort-Region bleibt stehen, Zonen bleiben gemerkt
await MO.markDeadZone(48.07, 11.96);
const beforeClear = stores.tiles.size;
await MO.clear();
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);
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); });

View file

@ -0,0 +1,87 @@
// 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');
// 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();
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);
if (stores.tiles.size !== 0) throw new Error('Komplett-Wipe unvollständig');
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); });