Offline-Karten Runde 3: Puls-Icon, rollendes Vorausladen, Ausschnitt-Download, Speicher-Cap
- Offline-Indikator: pulsierendes 32px-Icon oben rechts (unter Kopfzeilen-Hoehe) statt Leiste ueber die volle Breite — verdeckte '<- Zurueck' in der Routennavigation (Geraetetest Rene) - Rollendes Vorausladen: setGps laedt alle ~400m still fehlende z14+-2-Kacheln um die Position — deckt den Weg schon beim ERSTEN Funkloch-Besuch ab - Bereichsauswahl light: 'Sichtbaren Ausschnitt speichern' im Offline-Modal (downloadBbox, Cap 40 MB, Zu-gross-Schutz) - Speicher-Cap 250 MB als Soft-Guard fuer automatische Pfade + totalBytes-Zaehler + navigator.storage.persist() best-effort; echte LRU vertagt (Refcounting noetig) - Auto-OSM-Raster-Prefetch entfernt (manueller Leaflet-Pfad bleibt) - Logik-Tests (Node-Stubs) fuer Bbox/Cap/Throttle/persist bestanden Bump v1229
This commit is contained in:
parent
3426d2b7c8
commit
763108fa7c
10 changed files with 214 additions and 40 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1228
|
||||
1229
|
||||
|
|
@ -7307,12 +7307,27 @@ svg.empty-state-icon {
|
|||
pointer-events: none;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
/* Eingeklappt (5s nach Offline-Gang, boot.js): schmale Icon-Leiste statt 2-Zeilen-Banner —
|
||||
das volle Banner verdeckte die Karten-Steuerung oben (Gerätetest iOS 2026-06-06). */
|
||||
/* Eingeklappt (5s nach Offline-Gang, boot.js): kleines pulsierendes Icon oben rechts —
|
||||
die Leiste über die volle Breite verdeckte Nav-Elemente (z.B. „← Zurück" in der
|
||||
Routennavigation, Gerätetest 2026-06-07). Sitzt UNTERHALB der Kopfzeilen-Höhe,
|
||||
damit es Buttons (Zentrieren, Legende) nie überlagert. */
|
||||
#offline-banner.collapsed {
|
||||
padding: calc(env(safe-area-inset-top, 0px) + 2px) 16px 2px;
|
||||
top: calc(env(safe-area-inset-top, 0px) + 54px);
|
||||
left: auto;
|
||||
right: 8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,.35);
|
||||
animation: by-offline-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
#offline-banner.collapsed #offline-banner-text { display: none; }
|
||||
#offline-banner.collapsed #offline-queue-badge { display: none !important; }
|
||||
@keyframes by-offline-pulse {
|
||||
0%, 100% { opacity: 0.55; transform: scale(1); }
|
||||
50% { opacity: 1; transform: scale(1.1); }
|
||||
}
|
||||
#offline-banner.collapsed #offline-banner-text { display: none; }
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
STREAK-WIDGET (Welcome-Seite)
|
||||
|
|
|
|||
|
|
@ -86,14 +86,14 @@
|
|||
<title>Ban Yaro</title>
|
||||
|
||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||
<script src="/js/boot-early.js?v=1228"></script>
|
||||
<script src="/js/boot-early.js?v=1229"></script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1228">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1228">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1228">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1228">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1228">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1229">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1229">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1229">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1229">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1229">
|
||||
</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=1228"></script>
|
||||
<script src="/js/ui.js?v=1228"></script>
|
||||
<script src="/js/app.js?v=1228"></script>
|
||||
<script src="/js/worlds.js?v=1228"></script>
|
||||
<script src="/js/offline-indicator.js?v=1228"></script>
|
||||
<script src="/js/api.js?v=1229"></script>
|
||||
<script src="/js/ui.js?v=1229"></script>
|
||||
<script src="/js/app.js?v=1229"></script>
|
||||
<script src="/js/worlds.js?v=1229"></script>
|
||||
<script src="/js/offline-indicator.js?v=1229"></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=1228"></script>
|
||||
<script src="/js/boot.js?v=1229"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '1228'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '1229'; // ← 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;
|
||||
|
|
|
|||
|
|
@ -234,6 +234,7 @@ window.MapOffline = (function () {
|
|||
function downloadAround(lat, lon, opts) {
|
||||
if (typeof opts === 'number') opts = {}; // alte Signatur (lat, lon, radiusKm) → Default-Budget
|
||||
opts = opts || {};
|
||||
_persistStorage();
|
||||
var budget = (opts.budgetMB || 5) * 1048576;
|
||||
var maxKm = opts.maxRadiusKm || 25;
|
||||
var cx = _x(lon, MAXZOOM), cy = _y(lat, MAXZOOM);
|
||||
|
|
@ -293,6 +294,7 @@ window.MapOffline = (function () {
|
|||
return _addRegion({ lat: lat, lon: lon, radiusKm: radiusKm, 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, radiusKm: radiusKm }; });
|
||||
}
|
||||
|
||||
|
|
@ -301,7 +303,73 @@ window.MapOffline = (function () {
|
|||
// wird nie hochgeladen. Signal = echte Tile-Fetch-Fehler bei aktivem GPS
|
||||
// (NICHT navigator.onLine — das lügt bei Captive-Portal/Schwachempfang).
|
||||
var _gps = null, _lastZoneNote = 0;
|
||||
function setGps(pos) { _gps = pos; } // {lat,lon} während aktiver Aufzeichnung, sonst null
|
||||
|
||||
// ---- Speicher-Cap (Soft-Guard für die AUTOMATISCHEN Pfade) -------------------
|
||||
// Manuelle Downloads bleiben immer möglich; Vorausladen + Funkloch-Autofill stoppen
|
||||
// über dem Cap. totalBytes wird bei jedem Download mitgezählt; clear() setzt zurück.
|
||||
var CAP_MB = 250;
|
||||
function _bumpTotal(bytes) {
|
||||
if (!bytes) return Promise.resolve();
|
||||
return _metaGet('totalBytes')
|
||||
.then(function (t) { return _metaPut('totalBytes', (t || 0) + bytes); })
|
||||
.catch(function () {});
|
||||
}
|
||||
function _overCap() {
|
||||
return _metaGet('totalBytes')
|
||||
.then(function (t) { return (t || 0) > CAP_MB * 1048576; })
|
||||
.catch(function () { return false; });
|
||||
}
|
||||
|
||||
// Persistenten Speicher anfragen (best-effort, idempotent) — härtet IndexedDB gegen
|
||||
// Eviction bei Speicherdruck. Safari/iOS ignoriert es teils, schadet aber nicht.
|
||||
function _persistStorage() {
|
||||
if (_persistStorage._done) return;
|
||||
_persistStorage._done = true;
|
||||
try {
|
||||
if (navigator.storage && navigator.storage.persist) navigator.storage.persist().catch(function () {});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// {lat,lon} während aktiver Aufzeichnung, sonst null. Nebeneffekt (Runde 3):
|
||||
// ROLLENDES VORAUSLADEN — solange Empfang da ist, alle ~400 m die fehlenden Kacheln
|
||||
// um die aktuelle Position still mitnehmen. Deckt den Weg + die Anfahrt ab, BEVOR
|
||||
// man ins Funkloch läuft (greift schon beim ersten Besuch, anders als das Gedächtnis).
|
||||
var _lastPre = null, _preActive = false;
|
||||
function setGps(pos) {
|
||||
_gps = pos;
|
||||
if (!pos) { _lastPre = null; return; }
|
||||
if (_preActive || !navigator.onLine) return;
|
||||
if (_lastPre && _distKm(_lastPre.lat, _lastPre.lon, pos.lat, pos.lon) < 0.4) return;
|
||||
_preActive = true;
|
||||
var p = { lat: pos.lat, lon: pos.lon };
|
||||
_overCap().then(function (over) {
|
||||
if (over) return;
|
||||
return _prefetchRing(p.lat, p.lon, 2).then(function () { _lastPre = p; });
|
||||
}).catch(function () {})
|
||||
.then(function () { _preActive = false; });
|
||||
}
|
||||
|
||||
// z14-Kacheln ±n um lat/lon (+ Eltern z10–13) — NUR fehlende, still, ohne Region-Eintrag.
|
||||
function _prefetchRing(lat, lon, n) {
|
||||
var cx = _x(lon, MAXZOOM), cy = _y(lat, MAXZOOM), seen = {}, list = [];
|
||||
for (var x = cx - n; x <= cx + n; x++) for (var y = cy - n; y <= cy + n; y++) {
|
||||
list.push([MAXZOOM, x, y]);
|
||||
for (var pz = 13; pz >= 10; pz--) {
|
||||
var px = x >> (MAXZOOM - pz), py = y >> (MAXZOOM - pz), k = pz + '/' + px + '/' + py;
|
||||
if (!seen[k]) { seen[k] = 1; list.push([pz, px, py]); }
|
||||
}
|
||||
}
|
||||
var missing = [], chain = Promise.resolve();
|
||||
list.forEach(function (t) {
|
||||
chain = chain.then(function () {
|
||||
return _get(t[0] + '/' + t[1] + '/' + t[2]).then(function (hit) { if (!hit) missing.push(t); });
|
||||
});
|
||||
});
|
||||
var state = { done: 0, bytes: 0, stored: 0 };
|
||||
return chain
|
||||
.then(function () { return missing.length ? _fetchTiles(missing, state, null) : null; })
|
||||
.then(function () { return _bumpTotal(state.bytes); });
|
||||
}
|
||||
|
||||
function _distKm(aLat, aLon, bLat, bLon) {
|
||||
var dLat = (bLat - aLat) * 111, dLon = (bLon - aLon) * 111 * Math.cos(aLat * Math.PI / 180);
|
||||
|
|
@ -335,7 +403,11 @@ window.MapOffline = (function () {
|
|||
if (_autofillActive || !navigator.onLine) return Promise.resolve(0);
|
||||
_autofillActive = true;
|
||||
var filled = 0;
|
||||
return _metaGet('deadzones').then(function (zones) {
|
||||
return _overCap().then(function (over) {
|
||||
if (over) return null; // Speicher-Cap erreicht → kein automatisches Nachladen mehr
|
||||
return _metaGet('deadzones');
|
||||
}).then(function (zones) {
|
||||
if (zones === null) return 0;
|
||||
zones = zones || [];
|
||||
var open = zones.filter(function (z) { return !z.filled; });
|
||||
if (!open.length) return 0;
|
||||
|
|
@ -360,6 +432,7 @@ window.MapOffline = (function () {
|
|||
function downloadCorridor(track, opts) {
|
||||
opts = opts || {};
|
||||
if (!track || track.length < 2) return Promise.reject(new Error('Kein GPS-Track'));
|
||||
_persistStorage();
|
||||
var buffer = opts.bufferKm || 1, cap = (opts.capMB || 50) * 1048576;
|
||||
var seen = {}, list = [];
|
||||
var push = function (z, x, y) {
|
||||
|
|
@ -403,6 +476,47 @@ window.MapOffline = (function () {
|
|||
return _addRegion({ type: 'korridor', name: opts.name || null, lat: track[0].lat, lon: track[0].lon,
|
||||
tiles: state.stored, bytes: state.bytes, pois: poiCount, savedAt: Date.now() });
|
||||
})
|
||||
.then(function () { return _bumpTotal(state.bytes); })
|
||||
.then(function () { return { tiles: state.stored, bytes: state.bytes, pois: poiCount, capped: state.bytes >= cap }; });
|
||||
}
|
||||
|
||||
// ---- Bereichsauswahl: sichtbaren Karten-Ausschnitt komplett speichern ---------
|
||||
// bbox = {south,west,north,east} (z.B. aktueller Viewport). Zu-groß-Schutz über
|
||||
// Kachelzahl, Abbruch-Cap über capMB. opts {capMB:40, name, onProgress({bytes,done,total})}.
|
||||
function downloadBbox(bbox, opts) {
|
||||
opts = opts || {};
|
||||
_persistStorage();
|
||||
var cap = (opts.capMB || 40) * 1048576;
|
||||
var seen = {}, list = [];
|
||||
var push = function (z, x, y) {
|
||||
if (x < 0 || y < 0 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) return;
|
||||
var k = z + '/' + x + '/' + y;
|
||||
if (!seen[k]) { seen[k] = 1; list.push([z, x, y]); }
|
||||
};
|
||||
for (var z = 0; z <= MAXZOOM; z++) {
|
||||
var x0 = _x(bbox.west, z), x1 = _x(bbox.east, z), y0 = _y(bbox.north, z), y1 = _y(bbox.south, z);
|
||||
if (z === MAXZOOM && (x1 - x0 + 1) * (y1 - y0 + 1) > 4000) {
|
||||
return Promise.reject(new Error('Bereich zu groß — bitte weiter reinzoomen.'));
|
||||
}
|
||||
for (var x = x0; x <= x1; x++) for (var y = y0; y <= y1; y++) push(z, x, y);
|
||||
}
|
||||
var state = { done: 0, bytes: 0, stored: 0 }, total = list.length, poiCount = 0;
|
||||
function chunkLoop(idx) {
|
||||
if (idx >= list.length || state.bytes >= cap) return Promise.resolve();
|
||||
return _fetchTiles(list.slice(idx, idx + 64), state, function () {
|
||||
if (opts.onProgress) opts.onProgress({ bytes: state.bytes, done: state.done, total: total });
|
||||
}).then(function () { return chunkLoop(idx + 64); });
|
||||
}
|
||||
var midLat = (bbox.south + bbox.north) / 2, midLon = (bbox.west + bbox.east) / 2;
|
||||
return chunkLoop(0)
|
||||
.then(function () { return _cacheGlyphs(); })
|
||||
.then(function (gb) { state.bytes += gb; return _cachePois(bbox); })
|
||||
.then(function (pc) {
|
||||
poiCount = pc;
|
||||
return _addRegion({ type: 'ausschnitt', name: opts.name || null, lat: midLat, lon: midLon,
|
||||
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 }; });
|
||||
}
|
||||
|
||||
|
|
@ -432,8 +546,10 @@ window.MapOffline = (function () {
|
|||
function stats() {
|
||||
return _count().then(function (count) {
|
||||
return _metaGet('regions').then(function (regions) {
|
||||
return _metaGet('region').then(function (meta) {
|
||||
return { count: count, meta: meta || null, regions: regions || [] };
|
||||
return _metaGet('totalBytes').then(function (totalBytes) {
|
||||
return _metaGet('region').then(function (meta) {
|
||||
return { count: count, meta: meta || null, regions: regions || [], totalBytes: totalBytes || 0 };
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -446,7 +562,7 @@ window.MapOffline = (function () {
|
|||
|
||||
return {
|
||||
registerProtocol: registerProtocol, downloadAround: downloadAround, downloadCorridor: downloadCorridor,
|
||||
tile: tile, glyph: glyph, pois: pois, alerts: alerts, coverage: coverage,
|
||||
downloadBbox: downloadBbox, tile: tile, glyph: glyph, pois: pois, alerts: alerts, coverage: coverage,
|
||||
setGps: setGps, markDeadZone: markDeadZone, autoFillDeadZones: autoFillDeadZones,
|
||||
stats: stats, hasRegion: hasRegion, clear: clear, MAXZOOM: MAXZOOM,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -417,9 +417,9 @@ window.OfflineIndicator = (() => {
|
|||
function init() {
|
||||
refresh();
|
||||
_prefetchPages();
|
||||
// OSM-Raster-Prefetch nur für die Leaflet-Karte — die GL-Karte (byt://-Vektorkacheln)
|
||||
// nutzt das Raster nicht. Komplett-Entfernung wenn Flag dauerhaft AN (OFFLINE_MAPS_PLAN.md).
|
||||
if (!_offlineTilesMode()) _prefetchTiles();
|
||||
// Automatischer OSM-Raster-Prefetch ENTFERNT (2026-06-07): Flag ist auf allen deployten
|
||||
// Hosts AN, die GL-Karte nutzt das Raster nicht. _prefetchTiles bleibt nur noch für den
|
||||
// manuellen „Fehlende nachladen"-Pfad im Leaflet-Modus (localhost / by_map_gl=0).
|
||||
_prefetchData();
|
||||
_bindLongPress();
|
||||
|
||||
|
|
|
|||
|
|
@ -2190,6 +2190,34 @@ window.Page_map = (() => {
|
|||
}
|
||||
}
|
||||
|
||||
// Bereichsauswahl: den SICHTBAREN Karten-Ausschnitt komplett speichern (z.B. fürs
|
||||
// Urlaubsziel: hinzoomen/-schieben, speichern). Cap 40 MB, Zu-groß-Schutz in MapOffline.
|
||||
async function _downloadViewport() {
|
||||
if (!_map || !window.MapOffline) return;
|
||||
const btn = document.getElementById('map-offline-btn');
|
||||
if (btn?.classList.contains('loading')) return;
|
||||
const p = _mapPaddedBounds(0.02);
|
||||
btn?.classList.add('loading');
|
||||
_setOsmStatus('Offline: 0 MB…');
|
||||
try {
|
||||
const res = await MapOffline.downloadBbox(
|
||||
{ south: p.south, west: p.west, north: p.north, east: p.east },
|
||||
{ capMB: 40, onProgress: pr => {
|
||||
_setOsmStatus(`Offline: ${(pr.bytes / 1048576).toFixed(1)} MB (${Math.round(pr.done / pr.total * 100)} %)…`);
|
||||
} });
|
||||
_setOsmStatus('');
|
||||
UI.toast.success(`Ausschnitt offline gespeichert — ${res.pois || 0} Marker, ${(res.bytes / 1048576).toFixed(1)} MB.`
|
||||
+ `${res.capped ? ' (40-MB-Limit erreicht)' : ''}`);
|
||||
window.OfflineIndicator?.refresh();
|
||||
if (_covOn) _setCoverage(true);
|
||||
} catch (e) {
|
||||
_setOsmStatus('');
|
||||
UI.toast.error(e?.message?.includes('zu groß') ? e.message : 'Offline-Download fehlgeschlagen — bitte erneut versuchen.');
|
||||
} finally {
|
||||
btn?.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Offline-Bereiche-Layer (gespeicherte z14-Kacheln) + Verwaltungs-Modal
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -2226,7 +2254,7 @@ window.Page_map = (() => {
|
|||
let s = { regions: [] };
|
||||
try { s = await MapOffline.stats(); } catch (e) {}
|
||||
const regions = s.regions || [];
|
||||
const totalBytes = regions.reduce((a, r) => a + (r.bytes || 0), 0);
|
||||
const totalBytes = s.totalBytes || regions.reduce((a, r) => a + (r.bytes || 0), 0);
|
||||
const totalPois = regions.reduce((a, r) => a + (r.pois || 0), 0);
|
||||
UI.modal.open({
|
||||
title: '🗺️ Offline-Karten',
|
||||
|
|
@ -2238,6 +2266,7 @@ window.Page_map = (() => {
|
|||
</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button class="btn btn-primary" id="off-dl">${UI.icon('download-simple')} Dieses Gebiet speichern (~5 MB)</button>
|
||||
<button class="btn btn-secondary" id="off-bbox">${UI.icon('squares-four')} Sichtbaren Ausschnitt speichern</button>
|
||||
<button class="btn btn-secondary" id="off-cov">${UI.icon('stack')} Gespeicherte Bereiche ${_covOn ? 'ausblenden' : 'anzeigen'}</button>
|
||||
${regions.length ? `<button class="btn btn-secondary" id="off-clear" style="color:var(--c-danger)">${UI.icon('trash')} Alles löschen</button>` : ''}
|
||||
</div>
|
||||
|
|
@ -2245,6 +2274,7 @@ window.Page_map = (() => {
|
|||
footer: `<button class="btn btn-secondary" data-modal-close style="width:100%">Schließen</button>`,
|
||||
});
|
||||
document.getElementById('off-dl')?.addEventListener('click', () => { UI.modal.close(); _downloadVectorRegion(); });
|
||||
document.getElementById('off-bbox')?.addEventListener('click', () => { UI.modal.close(); _downloadViewport(); });
|
||||
document.getElementById('off-cov')?.addEventListener('click', async () => { UI.modal.close(); await _setCoverage(!_covOn); });
|
||||
document.getElementById('off-clear')?.addEventListener('click', async e => {
|
||||
const btn = e.currentTarget;
|
||||
|
|
|
|||
|
|
@ -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=1228"></script>
|
||||
<script src="/js/landing-init.js?v=1229"></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">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
============================================================ */
|
||||
|
||||
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
||||
const VER = '1228';
|
||||
const VER = '1229';
|
||||
const CACHE_VERSION = `by-v${VER}`;
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
|
|
|
|||
|
|
@ -67,17 +67,30 @@ nach bestandenen Gerätetests Runde 1+2). localhost = Leaflet/AUS.
|
|||
bestanden) — er lag im bereits gespeicherten Gebiet. Nach dem Speichern werden die
|
||||
gespeicherten Bereiche jetzt blau auf der Routen-Detailkarte eingeblendet (`_detailMap._gl`).
|
||||
|
||||
**🔲 Offen (Runde 3):**
|
||||
- **Gerätetest Runde 2** (Budget-Download, Funkloch-Lernen auf echter Gassi-Runde, Korridor,
|
||||
Coverage-Layer) → dann Prod-Freigabe-Entscheidung (BY.offlineTiles-Default erweitern analog `by_map_gl`).
|
||||
- **Rollendes Vorausladen beim Aufzeichnen** (fortlaufend um die aktuelle Position cachen, solange
|
||||
Empfang da — deckt den Weg schon beim ersten Mal ab; Akku-/Datensparsamkeit beachten).
|
||||
- **Bereichsauswahl** (Karten-Ausschnitt/Rechteck als Download-Gebiet) — Korridor deckt den
|
||||
Hauptfall ab, Rest nach Bedarf.
|
||||
- **Speicher-Cap + LRU** über alles (alte Gebiete fliegen automatisch raus) + optional
|
||||
`navigator.storage.persist()`.
|
||||
- Alten OSM-Raster-Prefetch (`offline-indicator.js _prefetchTiles` + `map.js _cacheTiles`) komplett
|
||||
entfernen, wenn Flag dauerhaft AN (auch Prod).
|
||||
**✅ Runde 3 (2026-06-07):**
|
||||
- **Offline-Indikator = pulsierendes Icon** oben rechts (32 px, unterhalb der Kopfzeilen-Höhe) statt
|
||||
Banner über die volle Breite — verdeckte Nav-Elemente, z.B. „← Zurück" in der Routennavigation
|
||||
(Gerätetest René). Vollbanner weiterhin 5 s beim Offline-Gang, dann Icon.
|
||||
- **Rollendes Vorausladen beim Aufzeichnen:** `setGps()` lädt alle ~400 m still die FEHLENDEN
|
||||
z14±2-Kacheln (+Eltern) um die Position, solange online — deckt Weg + Anfahrt schon beim ERSTEN
|
||||
Besuch ab (das Funkloch-Gedächtnis greift erst ab dem 2.). Kein Region-Eintrag, kein UI.
|
||||
- **Bereichsauswahl light:** Modal-Button „Sichtbaren Ausschnitt speichern" → `downloadBbox(viewport,
|
||||
{capMB:40})` mit Zu-groß-Schutz (>4000 z14-Kacheln → „bitte reinzoomen").
|
||||
- **Speicher-Cap 250 MB (Soft-Guard):** `totalBytes`-Zähler in Meta; AUTOMATISCHE Pfade (Vorausladen,
|
||||
Funkloch-Autofill) stoppen über dem Cap, manuelle bleiben; `navigator.storage.persist()` best-effort.
|
||||
Echte LRU-Eviction bewusst vertagt (Kacheln werden regionsübergreifend geteilt → Eviction braucht
|
||||
Refcounting; bei ~8 MB/Gebiet kein Druck).
|
||||
- **Auto-OSM-Raster-Prefetch entfernt** (offline-indicator init); `_prefetchTiles`/`_cacheTiles`
|
||||
bleiben nur für den manuellen Leaflet-Pfad (localhost / by_map_gl=0).
|
||||
- Logik per Node-Stub-Tests verifiziert (Bbox, Zu-groß, Cap, Prefetch-Throttle, persist).
|
||||
Achtung Node 21+: eingebautes `navigator`-Global schluckt `global.navigator=`-Stubs —
|
||||
`Object.defineProperty(globalThis, 'navigator', …)` verwenden.
|
||||
|
||||
**🔲 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).
|
||||
- POIs auch beim rollenden Vorausladen (aktuell nur Kacheln; Giftköder kommen aus dem
|
||||
localStorage-Fallback der letzten Online-Position).
|
||||
|
||||
## Ziel
|
||||
GL-Vektorkarten offline-tauglich machen — Kernszenario **Gassi/Wandern im Funkloch**.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue