diff --git a/backend/main.py b/backend/main.py
index bf3a3bc..814445c 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -410,7 +410,7 @@ async def serve_media(path: str, request: _Request):
raise _HE(404, "Nicht gefunden.")
return _media_response(filepath)
-APP_VER = "1082" # muss mit APP_VER in app.js übereinstimmen
+APP_VER = "1083" # muss mit APP_VER in app.js übereinstimmen
@app.get("/.well-known/assetlinks.json")
async def assetlinks():
diff --git a/backend/static/css/components.css b/backend/static/css/components.css
index ee134f5..a26655f 100644
--- a/backend/static/css/components.css
+++ b/backend/static/css/components.css
@@ -8867,44 +8867,17 @@ svg.empty-state-icon {
}
/* ============================================================
- Offline-Bereitschafts-Indikator — schwebend über dem Welten-FAB
- Sichtbar NUR wenn Welten aktiv sind (Sibling-Selektor)
- 5 Pfade — Score 0 (grau) bis 5 (grün, gefüllt)
+ Offline-Bereitschafts-Anzeige IM Welten-FAB
+ Die 5 Pfoten-Pfade werden je nach Score grün gefärbt
+ (Default = weiß auf orange, filled = grün auf orange)
============================================================ */
-#offline-indicator {
- display: flex; /* Default: sichtbar — JS blendet auf Detail-Seiten aus */
- position: fixed;
- right: 20px; /* gleicher right wie #worlds-fab */
- bottom: calc(env(safe-area-inset-bottom, 16px) + 16px + 54px + 12px); /* FAB-Bottom + FAB-Höhe + 12px */
- width: 40px;
- height: 40px;
- border-radius: 50%;
- background: rgba(255,255,255,0.95);
- border: 2px solid var(--c-border);
- box-shadow: 0 2px 10px rgba(0,0,0,0.18);
- align-items: center;
- justify-content: center;
- padding: 0;
- cursor: pointer;
- z-index: 61; /* knapp über dem FAB (60), unter Modals */
- transition: transform 0.12s, opacity 0.2s;
+#worlds-fab .offline-paw .paw-elem {
+ color: #fff;
+ transition: stroke 0.4s ease, fill 0.4s ease;
}
-#offline-indicator.is-hidden { display: none; } /* JS-gesteuert: in Detail-Seiten */
-
-[data-theme="dark"] #offline-indicator {
- background: rgba(31,41,55,0.85);
- border-color: rgba(255,255,255,0.08);
-}
-#offline-indicator:active { transform: scale(0.92); }
-#offline-indicator .offline-paw { width: 24px; height: 24px; }
-
-.offline-paw .paw-elem {
- color: var(--c-text-muted);
- transition: stroke 0.5s ease, fill 0.5s ease;
-}
-.offline-paw .paw-elem.filled {
- color: var(--c-success);
- fill: var(--c-success);
+#worlds-fab .offline-paw .paw-elem.filled {
+ color: #16a34a; /* leuchtendes Grün, klar sichtbar auf orange */
+ fill: #16a34a;
}
.offline-status-row {
diff --git a/backend/static/index.html b/backend/static/index.html
index b8426aa..236ea78 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -101,9 +101,9 @@
-
-
-
+
+
+
@@ -602,27 +602,22 @@
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
@@ -630,11 +625,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index f882e5a..940be82 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 = '1082'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1083'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
const IS_STAGING = location.hostname === 'staging.banyaro.app';
// Cache-Bust-Parameter nach Update-Reload sofort entfernen.
diff --git a/backend/static/js/offline-indicator.js b/backend/static/js/offline-indicator.js
index ba2971d..5f8336b 100644
--- a/backend/static/js/offline-indicator.js
+++ b/backend/static/js/offline-indicator.js
@@ -1,7 +1,8 @@
/* ============================================================
- BAN YARO — Offline-Bereitschafts-Indikator
- 5-stufige Pfote im Header, zeigt wie viel der App offline
- verfügbar ist. Klick → Status-Modal mit Nachlade-Button.
+ BAN YARO — Offline-Bereitschafts-Anzeige IM Welten-FAB
+ Färbt die 5 Pfoten-Pfade je nach Cache-Stand grün:
+ 1 = App-Shell · 2 = Wichtige Seiten · 3 = Hund-/Tagebuchdaten
+ 4 = Karten-Tiles · 5 = Training & Wissen
============================================================ */
window.OfflineIndicator = (() => {
@@ -11,9 +12,8 @@ window.OfflineIndicator = (() => {
const CACHE_STATIC = `by-v${(window.APP_VER || '0')}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1';
const CACHE_API = 'ban-yaro-api-v1';
- const TILE_MIN = 50; // Mindest-Tiles für Stufe 4
+ const TILE_MIN = 50;
- // 5 Offline-Bereitschafts-Checks, in Reihenfolge der Pfoten-Stufen
const CHECKS = [
{ step: 1, title: 'App-Grundgerüst',
detail: 'CSS, Layout und Hauptmodule — die Basis',
@@ -27,8 +27,8 @@ window.OfflineIndicator = (() => {
if (!c) return false;
const must = ['diary.js','map.js','walks.js','erste-hilfe.js'];
const keys = await c.keys();
- const have = new Set(keys.map(r => r.url));
- return must.every(name => [...have].some(u => u.includes('/js/pages/' + name)));
+ const have = keys.map(r => r.url);
+ return must.every(name => have.some(u => u.includes('/js/pages/' + name)));
} },
{ step: 3, title: 'Hund- und Tagebuchdaten',
@@ -36,8 +36,7 @@ window.OfflineIndicator = (() => {
probe: async () => {
const c = await caches.open(CACHE_API).catch(() => null);
if (!c) return false;
- const keys = await c.keys();
- const urls = keys.map(r => r.url);
+ const urls = (await c.keys()).map(r => r.url);
return urls.some(u => /\/api\/dogs\/\d+/.test(u))
&& urls.some(u => /\/api\/dogs\/\d+\/diary/.test(u));
} },
@@ -47,12 +46,11 @@ window.OfflineIndicator = (() => {
probe: async () => {
const c = await caches.open(CACHE_TILES).catch(() => null);
if (!c) return false;
- const keys = await c.keys();
- return keys.length >= TILE_MIN;
+ return (await c.keys()).length >= TILE_MIN;
} },
{ step: 5, title: 'Training & Wissen',
- detail: 'Übungen, Wiki-Rassen, Wetter — Welt-Inhalte',
+ detail: 'Übungen, Wiki-Rassen, Wetter',
probe: async () => {
const c = await caches.open(CACHE_API).catch(() => null);
if (!c) return false;
@@ -62,47 +60,34 @@ window.OfflineIndicator = (() => {
} },
];
- let _btn = null;
- let _svg = null;
- let _lastScore = -1;
+ let _fab = null;
- // ----------------------------------------------------------
- // Score berechnen + Pfote einfärben
- // ----------------------------------------------------------
async function refresh() {
- if (!_btn) return;
- if (!('caches' in window)) { _btn.style.display = 'none'; return; }
+ _fab = document.getElementById('worlds-fab');
+ if (!_fab || !('caches' in window)) return null;
const results = await Promise.all(CHECKS.map(async c => {
try { return { ...c, ok: await c.probe() }; }
catch { return { ...c, ok: false }; }
}));
- const score = results.filter(r => r.ok).length;
- _applyScore(score, results);
- _lastScore = score;
- return { score, results };
- }
-
- function _applyScore(score, results) {
- if (!_svg) return;
- _svg.querySelectorAll('.paw-elem').forEach(el => {
+ _fab.querySelectorAll('.paw-elem').forEach(el => {
const step = Number(el.dataset.step);
const isOk = results.find(r => r.step === step)?.ok;
el.classList.toggle('filled', !!isOk);
});
- _btn.title = `Offline-Bereitschaft: ${score} von 5`;
- _btn.setAttribute('aria-label', `Offline-Bereitschaft: ${score} von 5`);
+
+ const score = results.filter(r => r.ok).length;
+ _fab.setAttribute('data-offline-score', `${score}/5`);
+ return { score, results };
}
- // ----------------------------------------------------------
- // Status-Modal beim Klick
- // ----------------------------------------------------------
- async function _openModal() {
+ // Optional aufrufbar: zeigt das Status-Modal mit Nachlade-Button
+ async function openStatus() {
const data = await refresh();
if (!data) return;
const { score, results } = data;
-
+ const missing = results.filter(r => !r.ok);
const rows = results.map(r => `
${r.ok ? '✓' : '○'}
@@ -113,25 +98,21 @@ window.OfflineIndicator = (() => {
`).join('');
- const missing = results.filter(r => !r.ok);
- const allOk = missing.length === 0;
-
UI.modal.open({
title: `🐾 Offline-Bereitschaft ${score}/5`,
body: `
- ${allOk
- ? 'Deine App ist voll offline-fähig. Du kannst Tagebuch, Karte und Daten auch ohne Internet nutzen.'
- : 'Je grüner deine Pfote, desto besser klappt die App ohne Internet. Fehlende Inhalte werden beim nächsten Online-Aufruf automatisch geladen.'}
+ ${missing.length === 0
+ ? 'Voll offline-fähig — Tagebuch, Karte und Daten funktionieren auch ohne Internet.'
+ : 'Je grüner deine Pfote im FAB, desto mehr klappt offline. Fehlende Inhalte werden beim nächsten Online-Aufruf automatisch geladen.'}
${rows}
`,
footer: `
- ${missing.length
- ? `
- ${UI.icon('cloud-arrow-down')} Fehlende jetzt nachladen
- ` : ''}
+ ${missing.length ? `
+ ${UI.icon('cloud-arrow-down')} Fehlende jetzt nachladen
+ ` : ''}
Schließen
`,
@@ -139,8 +120,7 @@ window.OfflineIndicator = (() => {
document.getElementById('offline-fill-btn')?.addEventListener('click', async () => {
const btn = document.getElementById('offline-fill-btn');
- btn.disabled = true;
- btn.textContent = 'Lade …';
+ btn.disabled = true; btn.textContent = 'Lade …';
await _fetchMissing(missing);
UI.modal.close();
UI.toast.success('Offline-Inhalte aktualisiert.');
@@ -148,14 +128,10 @@ window.OfflineIndicator = (() => {
});
}
- // ----------------------------------------------------------
- // Fehlende Inhalte aktiv nachladen
- // ----------------------------------------------------------
async function _fetchMissing(missing) {
const tasks = [];
for (const m of missing) {
if (m.step === 2) {
- // Page-Module fetchen → SW cached sie automatisch
['diary.js','map.js','walks.js','erste-hilfe.js'].forEach(p =>
tasks.push(fetch(`/js/pages/${p}?v=${window.APP_VER}`).catch(() => {})));
} else if (m.step === 3) {
@@ -165,17 +141,13 @@ window.OfflineIndicator = (() => {
tasks.push(fetch(`/api/dogs/${dogId}/diary?limit=20`).catch(() => {}));
}
} else if (m.step === 4) {
- // Karten-Tiles: SW per Message anstoßen
if (navigator.serviceWorker?.controller) {
const pos = await new Promise(res =>
navigator.geolocation?.getCurrentPosition(p => res(p), () => res(null), { timeout: 4000 }));
if (pos) {
navigator.serviceWorker.controller.postMessage({
- type: 'CACHE_TILES',
- lat: pos.coords.latitude,
- lon: pos.coords.longitude,
- zoom: 14,
- radius: 2,
+ type: 'CACHE_TILES', lat: pos.coords.latitude, lon: pos.coords.longitude,
+ zoom: 14, radius: 2,
});
}
}
@@ -188,39 +160,8 @@ window.OfflineIndicator = (() => {
await Promise.all(tasks);
}
- // ----------------------------------------------------------
- // Sichtbarkeit an Welten-Overlay koppeln
- // — default sichtbar; nur ausblenden wenn explizit auf Detail-Seite
- // ----------------------------------------------------------
- function _syncVisibility() {
- if (!_btn) return;
- const ov = document.getElementById('worlds-overlay');
- if (!ov) return; // ohne Welten-Overlay sichtbar lassen
- const inWorlds = ov.classList.contains('worlds-visible')
- || ov.style.display === 'block'
- || ov.style.display === '';
- _btn.classList.toggle('is-hidden', !inWorlds);
- }
-
- // ----------------------------------------------------------
- // Init
- // ----------------------------------------------------------
function init() {
- _btn = document.getElementById('offline-indicator');
- if (!_btn) { console.warn('[OfflineIndicator] #offline-indicator nicht im DOM'); return; }
- _svg = _btn.querySelector('.offline-paw');
- _btn.addEventListener('click', _openModal);
-
- // MutationObserver: Welten-Overlay Klassenänderung → Indikator zeigen/verstecken
- const ov = document.getElementById('worlds-overlay');
- if (ov) {
- _syncVisibility();
- new MutationObserver(_syncVisibility).observe(ov, { attributes: true, attributeFilter: ['class', 'style'] });
- }
-
refresh();
-
- // bei SW-Updates und alle 60s neu prüfen
if (navigator.serviceWorker) {
navigator.serviceWorker.addEventListener('message', e => {
if (e?.data?.type === 'CACHE_UPDATE') refresh();
@@ -229,7 +170,7 @@ window.OfflineIndicator = (() => {
setInterval(refresh, 60_000);
}
- return { init, refresh };
+ return { init, refresh, openStatus };
})();
if (document.readyState !== 'loading') {
diff --git a/backend/static/sw.js b/backend/static/sw.js
index c6b47d2..8d364e3 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 = '1082';
+const VER = '1083';
const CACHE_VERSION = `by-v${VER}`;
const CACHE_STATIC = `${CACHE_VERSION}-static`;
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten