diff --git a/Makefile b/Makefile
index 9402ea3..c16bf3f 100644
--- a/Makefile
+++ b/Makefile
@@ -287,7 +287,8 @@ bump:
sed -i.bak -E "s/const VER[[:space:]]*=[[:space:]]*'[0-9]+'/const VER = '$$NEW'/" backend/static/sw.js && rm -f backend/static/sw.js.bak; \
sed -i.bak -E "s/const APP_VER[[:space:]]*=[[:space:]]*'[0-9]+'/const APP_VER = '$$NEW'/" backend/static/js/app.js && rm -f backend/static/js/app.js.bak; \
sed -i.bak -E "s/\?v=[0-9]+/?v=$$NEW/g" backend/static/index.html && rm -f backend/static/index.html.bak; \
- echo " ✓ APP_VER $$CUR → $$NEW (VERSION, sw.js, app.js, index.html aktualisiert)"
+ sed -i.bak -E "s/\?v=[0-9]+/?v=$$NEW/g" backend/static/landing.html && rm -f backend/static/landing.html.bak; \
+ echo " ✓ APP_VER $$CUR → $$NEW (VERSION, sw.js, app.js, index.html, landing.html aktualisiert)"
# ----------------------------------------------------------
# TEST — Smoke-Tests gegen isolierte Test-DB (kein Docker, kein DS)
diff --git a/VERSION b/VERSION
index 39987d0..5b2b550 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1099
\ No newline at end of file
+1100
\ No newline at end of file
diff --git a/backend/main.py b/backend/main.py
index 569a887..b6beb40 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -110,8 +110,8 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
- "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://umami.motocamp.de; "
- "style-src 'self' 'unsafe-inline'; "
+ "script-src 'self' https://umami.motocamp.de; " # ohne unsafe-inline/eval — alle Inline-Scripts extrahiert
+ "style-src 'self' 'unsafe-inline'; " # Inline-Styles bleiben (zu viele Fundstellen für jetzt)
"img-src 'self' data: blob: https:; "
"connect-src 'self' https:; "
"frame-ancestors 'none'; "
diff --git a/backend/static/index.html b/backend/static/index.html
index bf2434e..86abf58 100644
--- a/backend/static/index.html
+++ b/backend/static/index.html
@@ -86,24 +86,12 @@
Ban Yaro
-
+
-
-
-
+
+
+
@@ -625,11 +613,11 @@
-
-
-
-
-
+
+
+
+
+
@@ -637,130 +625,9 @@
-
-
-
-
+
+
diff --git a/backend/static/js/app.js b/backend/static/js/app.js
index b275c15..7440658 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 = '1099'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
+const APP_VER = '1100'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
window.APP_VERSION = APP_VERSION;
diff --git a/backend/static/js/boot-early.js b/backend/static/js/boot-early.js
new file mode 100644
index 0000000..18fce13
--- /dev/null
+++ b/backend/static/js/boot-early.js
@@ -0,0 +1,13 @@
+/* Theme-Setup und theme-color für Status-Leiste.
+ MUSS synchron im VOR den CSS-Links laufen, sonst FOUC. */
+(function() {
+ var t = localStorage.getItem('by_theme');
+ var isDark = t === 'dark' || (t !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);
+ var isAndroid = /android/i.test(navigator.userAgent);
+ if (t === 'dark') document.documentElement.setAttribute('data-theme', 'dark');
+ if (t === 'light') document.documentElement.setAttribute('data-theme', 'light');
+ // Android: immer dunkel (Amber-Streifen nicht transparent möglich)
+ // iOS: black-translucent übernimmt das
+ var m = document.getElementById('meta-theme-color');
+ if (m) m.setAttribute('content', (isDark || isAndroid) ? '#0f1623' : '#C4843A');
+})();
diff --git a/backend/static/js/boot.js b/backend/static/js/boot.js
new file mode 100644
index 0000000..9f54463
--- /dev/null
+++ b/backend/static/js/boot.js
@@ -0,0 +1,127 @@
+/* ============================================================
+ BAN YARO — Boot-Phase
+ Offline-Banner + Service Worker Registration + Update-Flow
+ Extrahiert aus index.html für CSP-Härtung (kein unsafe-inline)
+ ============================================================ */
+
+// ----------------------------------------------------------
+// Offline-Banner
+// ----------------------------------------------------------
+(function() {
+ function _updateBanner() {
+ var banner = document.getElementById('offline-banner');
+ if (!banner) return;
+ banner.style.display = navigator.onLine ? 'none' : 'flex';
+ }
+ window.addEventListener('offline', function() {
+ _updateBanner();
+ // Einmaliger Hinweis pro Session: App im Vordergrund lassen
+ if (!sessionStorage.getItem('by_offline_hint_shown')) {
+ sessionStorage.setItem('by_offline_hint_shown', '1');
+ setTimeout(function() {
+ window.UI?.toast?.info(
+ 'App im Vordergrund lassen — so bleiben Offline-Funktionen wie GPS und Datenspeicherung aktiv.',
+ 8000
+ );
+ }, 800);
+ }
+ // Queue-Count abfragen
+ if (navigator.serviceWorker) {
+ navigator.serviceWorker.ready.then(function(reg) {
+ if (reg.active) reg.active.postMessage({ type: 'QUEUE_COUNT' });
+ });
+ }
+ });
+ window.addEventListener('online', function() {
+ _updateBanner();
+ var badge = document.getElementById('offline-queue-badge');
+ if (badge) badge.style.display = 'none';
+ // Queue abarbeiten
+ if (navigator.serviceWorker) {
+ navigator.serviceWorker.ready.then(function(reg) {
+ if (reg.active) reg.active.postMessage({ type: 'PROCESS_QUEUE' });
+ });
+ }
+ });
+ _updateBanner();
+})();
+
+// ----------------------------------------------------------
+// Service Worker Registration + Update-Flow
+// ----------------------------------------------------------
+if ('serviceWorker' in navigator) {
+ window.addEventListener('load', function() {
+ navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' })
+ .then(function(reg) {
+ function _watchSW(sw) {
+ if (!sw) return;
+ sw.addEventListener('statechange', function() {
+ if (sw.state === 'activated') {
+ if (sessionStorage.getItem('by_skip_sw_reload')) return;
+ window.location.replace('/?_t=' + Date.now());
+ }
+ });
+ }
+ reg.addEventListener('updatefound', function() { _watchSW(reg.installing); });
+ if (reg.installing) _watchSW(reg.installing);
+ reg.update();
+ })
+ .catch(function(err) { console.warn('SW Registration failed:', err); });
+ });
+
+ // App aus dem Hintergrund: erneut prüfen
+ document.addEventListener('visibilitychange', function() {
+ if (document.visibilityState === 'visible') {
+ navigator.serviceWorker.getRegistration().then(function(reg) { if (reg) reg.update(); });
+ }
+ });
+
+ // Backup: controllerchange falls updatefound nicht feuert
+ // NICHT registrieren wenn diese Seite selbst durch SW-Reload entstand
+ if (!window._BY_SW_RELOAD) {
+ navigator.serviceWorker.addEventListener('controllerchange', function() {
+ if (sessionStorage.getItem('by_skip_sw_reload')) {
+ sessionStorage.removeItem('by_skip_sw_reload');
+ return;
+ }
+ window.location.replace('/?_t=' + Date.now());
+ });
+ }
+
+ navigator.serviceWorker.addEventListener('message', function(e) {
+ if (e.data && e.data.type === 'QUEUE_PROCESSED') {
+ var synced = e.data.synced, failed = e.data.failed, total = e.data.total;
+ if (total === 0) return;
+ if (synced > 0 && window.UI && window.UI.toast) {
+ window.UI.toast.success(
+ synced === 1
+ ? '1 offline gespeicherter Eintrag synchronisiert'
+ : synced + ' offline gespeicherte Einträge synchronisiert'
+ );
+ if (window.App && window.App.state && window.pages) {
+ var p = window.pages[window.App.state.page];
+ if (p && p.module && p.module.refresh) p.module.refresh();
+ }
+ }
+ if (failed > 0 && window.UI && window.UI.toast) {
+ window.UI.toast.warning(failed + ' Eintrag' + (failed > 1 ? 'e' : '') + ' noch nicht synchronisiert — kein Netz');
+ }
+ return;
+ }
+ if (e.data && e.data.type === 'QUEUE_COUNT') {
+ var badge = document.getElementById('offline-queue-badge');
+ if (badge) {
+ if (e.data.count > 0) {
+ badge.textContent = e.data.count;
+ badge.style.display = '';
+ } else {
+ badge.style.display = 'none';
+ }
+ }
+ return;
+ }
+ if (e.data && e.data.type === 'CHECK_NEARBY_ALERTS') {
+ if (window.App && window.App._checkNearbyAlerts) window.App._checkNearbyAlerts();
+ }
+ });
+}
diff --git a/backend/static/js/landing-init.js b/backend/static/js/landing-init.js
new file mode 100644
index 0000000..b4bcb53
--- /dev/null
+++ b/backend/static/js/landing-init.js
@@ -0,0 +1,99 @@
+/* ============================================================
+ BAN YARO — Landing Page Init
+ Dark-Mode-Check, Scroll-Animationen, Live-Stats, Stay-In-App
+ Extrahiert aus landing.html für CSP-Härtung
+ ============================================================ */
+
+// Dark Mode (CSS-Klasse)
+(function() {
+ var mq = window.matchMedia('(prefers-color-scheme: dark)');
+ if (mq.matches) document.documentElement.classList.add('dark');
+ mq.addEventListener('change', function(e) {
+ document.documentElement.classList.toggle('dark', e.matches);
+ });
+})();
+
+document.addEventListener('DOMContentLoaded', function() {
+
+ // App-Links: kein Redirect-Loop (ersetzt onclick="sessionStorage.setItem(...)")
+ document.querySelectorAll('[data-stay-in-app]').forEach(function(el) {
+ el.addEventListener('click', function() {
+ sessionStorage.setItem('by_stay_in_app', '1');
+ });
+ });
+
+ // Hundebesitzer-Details-Toggle (ersetzt inline onclick)
+ document.querySelectorAll('[data-toggle-target]').forEach(function(el) {
+ el.addEventListener('click', function() {
+ var c = document.getElementById(el.dataset.toggleTarget);
+ if (!c) return;
+ c.classList.toggle('open');
+ var open = c.classList.contains('open');
+ var openTxt = el.dataset.toggleTextOpen || '▴ Weniger anzeigen';
+ var closeTxt = el.dataset.toggleTextClose || el.textContent;
+ if (!el.dataset.toggleTextClose) el.dataset.toggleTextClose = closeTxt;
+ el.textContent = open ? openTxt : el.dataset.toggleTextClose;
+ });
+ });
+
+ // Auch ältere App-Links erfassen (Fallback ohne data-stay-in-app)
+ document.querySelectorAll('a[href="/"], a[href^="/#"]').forEach(function(a) {
+ a.addEventListener('click', function() {
+ sessionStorage.setItem('by_stay_in_app', '1');
+ });
+ });
+
+ // Scroll-Animationen
+ var _observer = new IntersectionObserver(function(entries) {
+ entries.forEach(function(e) {
+ if (e.isIntersecting) {
+ e.target.classList.add('visible');
+ _observer.unobserve(e.target);
+ }
+ });
+ }, { threshold: 0.12 });
+
+ document.querySelectorAll('.outcome-card, .feature-card, .usp-item, .pricing-card').forEach(function(el) {
+ el.classList.add('fade-up');
+ _observer.observe(el);
+ });
+ document.querySelectorAll('.fade-up').forEach(function(el) {
+ _observer.observe(el);
+ });
+
+ // Live-Zahlen
+ var fmt = new Intl.NumberFormat('de-DE');
+ fetch('/api/stats/public')
+ .then(function(r) { return r.json(); })
+ .then(function(d) {
+ function set(id, val) {
+ var el = document.getElementById(id);
+ if (el) el.textContent = fmt.format(val);
+ }
+ set('big-users', d.users);
+ set('big-dogs', d.dogs);
+ set('big-km', d.km);
+ set('big-posts', d.forum_posts);
+ set('big-diary', d.diary_entries);
+ set('big-kotbeutel', d.kotbeutel);
+
+ var heroStats = document.getElementById('hero-stats');
+ if (!heroStats || !d.users) return;
+
+ var items = [
+ { val: d.users, label: 'Hundemenschen' },
+ { val: d.dogs, label: 'Hunde' },
+ { val: d.km, label: 'km Gassi-Wege' },
+ { val: d.diary_entries, label: 'Tagebuch-Einträge' },
+ { val: d.kotbeutel, label: 'Mülleimer für Kotbeutel'},
+ ];
+ items.sort(function(a, b) { return a.val - b.val; });
+
+ heroStats.innerHTML = items.map(function(item, i) {
+ return (i > 0 ? '·' : '') +
+ '' + fmt.format(item.val) + ' ' + item.label;
+ }).join('');
+ heroStats.style.display = 'flex';
+ })
+ .catch(function() {});
+});
diff --git a/backend/static/landing.html b/backend/static/landing.html
index 3e8eaa5..273e2cd 100644
--- a/backend/static/landing.html
+++ b/backend/static/landing.html
@@ -4,13 +4,7 @@
-
+
Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz
@@ -731,7 +725,7 @@
Weil jeder Moment mit ihm zählt.
Ban Yaro begleitet euch durch jeden gemeinsamen Tag — Tagebuch, Training und Gesundheit für Hundebesitzer, Stammbaum und Wurfverwaltung für Züchter. Eine App. Mit ganzem Herzen.