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.

- Kostenlos starten + Kostenlos starten Ich bin Züchter
@@ -849,7 +843,7 @@

Kostenlos starten + data-stay-in-app>Kostenlos starten
@@ -951,7 +945,9 @@ - + Alle Features für Hundebesitzer ansehen ▾ + + Alle Features für Hundebesitzer ansehen ▾
@@ -1402,7 +1398,7 @@
  • Persönlichkeitstest, Adoption, Ausgaben
  • Kostenlos starten + data-stay-in-app>Kostenlos starten
    @@ -1419,7 +1415,7 @@
  • Alles aus Kostenlos inklusive
  • Pro starten + data-stay-in-app>Pro starten
    @@ -1606,70 +1602,6 @@ - diff --git a/backend/static/sw.js b/backend/static/sw.js index 51b4ac9..47190ee 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 = '1099'; +const VER = '1100'; const CACHE_VERSION = `by-v${VER}`; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten @@ -36,6 +36,8 @@ const STATIC_ASSETS = [ `/js/app.js?v=${VER}`, `/js/worlds.js?v=${VER}`, `/js/offline-indicator.js?v=${VER}`, + `/js/boot-early.js?v=${VER}`, + `/js/boot.js?v=${VER}`, '/js/leaflet.markercluster.js', '/css/MarkerCluster.css', '/css/MarkerCluster.Default.css',