Security: CSP gehärtet — unsafe-inline + unsafe-eval raus, SW by-v1100
Inline-Scripts extrahiert:
- boot-early.js: Theme + theme-color (synchron im <head>, VOR CSS)
- boot.js: Offline-Banner + Service-Worker-Registration + Update-Flow
- landing-init.js: Dark-mode + Scroll-Animationen + Live-Stats +
Stay-In-App-Handler + Details-Toggle
Inline onclick-Handler in landing.html:
- 5× sessionStorage.setItem('by_stay_in_app','1') → data-stay-in-app
- 1× Details-Toggle → data-toggle-target + data-toggle-text-open
- JS-Handler in landing-init.js binden die data-Attribute
CSP-Header (main.py):
- script-src: 'unsafe-inline' und 'unsafe-eval' entfernt
- style-src 'unsafe-inline' bleibt (Inline-Styles bleiben für jetzt,
zu viele Fundstellen)
- Umami bleibt whitelisted
SW STATIC_ASSETS erweitert um boot-early.js + boot.js.
make bump aktualisiert jetzt auch landing.html ?v= Anker.
Tests grün (19/19).
This commit is contained in:
parent
15d319fbd5
commit
65cfa25e59
10 changed files with 267 additions and 226 deletions
|
|
@ -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;
|
||||
|
|
|
|||
13
backend/static/js/boot-early.js
Normal file
13
backend/static/js/boot-early.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/* Theme-Setup und theme-color für Status-Leiste.
|
||||
MUSS synchron im <head> 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');
|
||||
})();
|
||||
127
backend/static/js/boot.js
Normal file
127
backend/static/js/boot.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
99
backend/static/js/landing-init.js
Normal file
99
backend/static/js/landing-init.js
Normal file
|
|
@ -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 ? '<span class="sep">·</span>' : '') +
|
||||
'<strong>' + fmt.format(item.val) + '</strong> ' + item.label;
|
||||
}).join('');
|
||||
heroStats.style.display = 'flex';
|
||||
})
|
||||
.catch(function() {});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue