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:
rene 2026-05-27 06:23:47 +02:00
parent 15d319fbd5
commit 65cfa25e59
10 changed files with 267 additions and 226 deletions

View file

@ -86,24 +86,12 @@
<title>Ban Yaro</title>
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
<script>
(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 möglich transparent zu machen)
// iOS: black-translucent übernimmt das
var m = document.getElementById('meta-theme-color');
if (m) m.setAttribute('content', (isDark || isAndroid) ? '#0f1623' : '#C4843A');
})();
</script>
<script src="/js/boot-early.js?v=1100"></script>
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
<link rel="stylesheet" href="/css/design-system.css?v=1099">
<link rel="stylesheet" href="/css/layout.css?v=1099">
<link rel="stylesheet" href="/css/components.css?v=1099">
<link rel="stylesheet" href="/css/design-system.css?v=1100">
<link rel="stylesheet" href="/css/layout.css?v=1100">
<link rel="stylesheet" href="/css/components.css?v=1100">
</head>
<body>
@ -625,11 +613,11 @@
<div id="modal-container"></div>
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
<script src="/js/api.js?v=1099"></script>
<script src="/js/ui.js?v=1099"></script>
<script src="/js/app.js?v=1099"></script>
<script src="/js/worlds.js?v=1099"></script>
<script src="/js/offline-indicator.js?v=1099"></script>
<script src="/js/api.js?v=1100"></script>
<script src="/js/ui.js?v=1100"></script>
<script src="/js/app.js?v=1100"></script>
<script src="/js/worlds.js?v=1100"></script>
<script src="/js/offline-indicator.js?v=1100"></script>
<!-- Feature-Seiten werden lazy geladen -->
@ -637,130 +625,9 @@
<script defer src="/stats/script.js" data-website-id="d1b5fe13-0e6f-4461-a176-c5439cbbc27f" data-api-host="/stats"></script>
<!-- Offline-Banner Logik -->
<script>
(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' });
});
}
});
// Initial prüfen
_updateBanner();
})();
</script>
<!-- Service Worker -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' })
.then(reg => {
function _watchSW(sw) {
if (!sw) return;
sw.addEventListener('statechange', () => {
if (sw.state === 'activated') {
// Flag nur prüfen, nicht konsumieren — controllerchange konsumiert ihn
if (sessionStorage.getItem('by_skip_sw_reload')) return;
window.location.replace('/?_t=' + Date.now());
}
});
}
// Listener VOR update() registrieren — verhindert Race Condition
reg.addEventListener('updatefound', () => _watchSW(reg.installing));
// Falls SW bereits installiert (Seite wurde nach SW-Install neu geladen)
if (reg.installing) _watchSW(reg.installing);
reg.update();
})
.catch(err => console.warn('SW Registration failed:', err));
});
// Backup: erneut prüfen wenn App aus dem Hintergrund kommt
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
navigator.serviceWorker.getRegistration().then(reg => reg?.update());
}
});
// Backup: controllerchange (falls updatefound nicht feuert)
// NICHT registrieren wenn diese Seite selbst durch einen SW-Reload entstand (_t= im URL)
// — verhindert Dauerschleife wenn clients.claim() erst nach Seitenstart feuert
if (!window._BY_SW_RELOAD) {
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (sessionStorage.getItem('by_skip_sw_reload')) {
sessionStorage.removeItem('by_skip_sw_reload');
return;
}
window.location.replace('/?_t=' + Date.now());
});
}
navigator.serviceWorker.addEventListener('message', e => {
if (e.data?.type === 'QUEUE_PROCESSED') {
const { synced, failed, total } = e.data;
if (total === 0) return;
if (synced > 0 && window.UI?.toast) {
window.UI.toast.success(
synced === 1
? '1 offline gespeicherter Eintrag synchronisiert'
: `${synced} offline gespeicherte Einträge synchronisiert`
);
// Aktuelle Seite neu laden
window.App?.state && window.pages?.[window.App.state.page]?.module?.refresh?.();
}
if (failed > 0 && window.UI?.toast) {
window.UI.toast.warning(`${failed} Eintrag${failed > 1 ? 'e' : ''} noch nicht synchronisiert — kein Netz`);
}
return;
}
if (e.data?.type === 'QUEUE_COUNT') {
const 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?.type === 'CHECK_NEARBY_ALERTS') {
window.App?._checkNearbyAlerts?.();
}
});
}
</script>
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
<script src="/js/boot.js?v=1100"></script>
</body>