fix(pwa): enable crossorigin=use-credentials for manifest, add id and header install trigger

This commit is contained in:
Moritz Graf
2026-10-12 08:16:03 +02:00
parent 545752fa60
commit f5b5deaae7
4 changed files with 133 additions and 36 deletions
+77 -29
View File
@@ -464,9 +464,9 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
</div>
"""
# PWA Manifest (RFC-compliant base64 data URI + external webmanifest)
import base64
# PWA Manifest
manifest_dict = {
"id": "/family/mila.html",
"name": "Mila — Schlaf & Entwicklung",
"short_name": "Mila",
"description": "Schlaf-, Trink- und Windeltagebuch für Mila Graf",
@@ -481,13 +481,25 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
"src": "/family/mila-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
"purpose": "any"
},
{
"src": "/family/mila-icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/family/mila-icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
"purpose": "any"
},
{
"src": "/family/mila-icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/family/mila-apple-touch-icon.png",
@@ -501,8 +513,6 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
}
]
}
manifest_json = json.dumps(manifest_dict)
manifest_uri = f"data:application/manifest+json;base64,{base64.b64encode(manifest_json.encode('utf-8')).decode('utf-8')}"
# Full HTML Document
html = f"""<!DOCTYPE html>
@@ -523,8 +533,7 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
<meta name="theme-color" content="#090A0F">
<meta name="msapplication-TileColor" content="#090A0F">
<link rel="manifest" href="/family/mila.webmanifest">
<link rel="manifest" href="{manifest_uri}">
<link rel="manifest" href="/family/mila.webmanifest" crossorigin="use-credentials">
<link rel="apple-touch-icon" href="/family/mila-apple-touch-icon.png">
<link rel="apple-touch-icon" sizes="180x180" href="/family/mila-apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="192x192" href="/family/mila-icon-192.png">
@@ -615,9 +624,13 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
</div>
</div>
<!-- Live State Badge & Manual Refresh Button -->
<!-- Live State Badge, Install Button & Manual Refresh Button -->
<div class="flex items-center gap-2">
{'<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-purple-500/15 border border-purple-500/30 text-purple-300 text-xs font-semibold shadow-sm shadow-purple-500/10"><span class="w-2 h-2 rounded-full bg-purple-400 animate-pulse"></span><span>Schl\u00e4ft (<span class="live-sleep-timer" data-start-iso="' + active_sleep_iso + '">' + active_sleep_duration_str + '</span>)</span></div>' if is_sleeping else '<div class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-emerald-500/15 border border-emerald-500/30 text-emerald-300 text-xs font-semibold shadow-sm shadow-emerald-500/10"><span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span><span>Wach (<span class="live-wake-timer" data-end-iso="' + last_wake_iso + '">' + wake_window_str + '</span>)</span></div>'}
<button id="header-pwa-install-btn" onclick="triggerPwaInstall()" class="hidden inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-xl bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/30 text-purple-300 text-xs font-semibold transition" title="Mila App installieren">
<i data-lucide="download" class="w-3.5 h-3.5"></i>
<span class="hidden sm:inline">Installieren</span>
</button>
<button id="manual-refresh-btn" onclick="triggerManualRefresh()" title="Neu laden" class="p-2 rounded-xl bg-zinc-900/80 border border-zinc-800 text-zinc-400 hover:text-white hover:border-purple-500/40 hover:bg-zinc-800 transition">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
</button>
@@ -775,44 +788,54 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
lucide.createIcons();
}});
// Service Worker Registration for PWA installability
// Service Worker Registration for PWA installability & offline support
if ('serviceWorker' in navigator) {{
window.addEventListener('load', () => {{
navigator.serviceWorker.register('/family/sw.js').catch(() => {{
navigator.serviceWorker.register('sw.js').catch(console.error);
}});
navigator.serviceWorker.register('/family/sw.js', {{ scope: '/family/' }})
.then(reg => console.log('[PWA] Service Worker registered:', reg.scope))
.catch(err => console.warn('[PWA] Service Worker registration failed:', err));
}});
}}
// Chrome/Android PWA BeforeInstallPrompt
// Chrome/Android/Desktop PWA BeforeInstallPrompt & iOS Detection
let deferredInstallPrompt = null;
const isIos = (/iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) && !window.MSStream;
const isStandalone = window.navigator.standalone === true || window.matchMedia('(display-mode: standalone)').matches;
window.addEventListener('beforeinstallprompt', (e) => {{
e.preventDefault();
deferredInstallPrompt = e;
const banner = document.getElementById('pwa-install-banner');
if (banner) banner.classList.remove('hidden');
if (banner && !isStandalone) banner.classList.remove('hidden');
const headerBtn = document.getElementById('header-pwa-install-btn');
if (headerBtn && !isStandalone) headerBtn.classList.remove('hidden');
}});
window.addEventListener('appinstalled', () => {{
console.log('[PWA] App successfully installed');
const banner = document.getElementById('pwa-install-banner');
if (banner) banner.classList.add('hidden');
const headerBtn = document.getElementById('header-pwa-install-btn');
if (headerBtn) headerBtn.classList.add('hidden');
deferredInstallPrompt = null;
}});
const installBtn = document.getElementById('pwa-install-btn');
if (installBtn) {{
installBtn.addEventListener('click', async () => {{
if (!deferredInstallPrompt) return;
deferredInstallPrompt.prompt();
const {{ outcome }} = await deferredInstallPrompt.userChoice;
if (outcome === 'accepted') {{
const banner = document.getElementById('pwa-install-banner');
if (banner) banner.classList.add('hidden');
}}
deferredInstallPrompt = null;
installBtn.addEventListener('click', () => {{
triggerPwaInstall();
}});
}}
// iOS Safari Detection & Guidance Banner
const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const isStandalone = window.navigator.standalone === true || window.matchMedia('(display-mode: standalone)').matches;
if (isIos && !isStandalone && !localStorage.getItem('dismissed_ios_mila_pwa')) {{
const iosBanner = document.getElementById('ios-install-banner');
if (iosBanner) iosBanner.classList.remove('hidden');
if (isIos && !isStandalone) {{
const headerBtn = document.getElementById('header-pwa-install-btn');
if (headerBtn) headerBtn.classList.remove('hidden');
if (!localStorage.getItem('dismissed_ios_mila_pwa')) {{
const iosBanner = document.getElementById('ios-install-banner');
if (iosBanner) iosBanner.classList.remove('hidden');
}}
}}
function dismissIosBanner() {{
@@ -821,6 +844,31 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
if (iosBanner) iosBanner.classList.add('hidden');
}}
window.triggerPwaInstall = function() {{
if (deferredInstallPrompt) {{
deferredInstallPrompt.prompt();
deferredInstallPrompt.userChoice.then(({{ outcome }}) => {{
if (outcome === 'accepted') {{
const banner = document.getElementById('pwa-install-banner');
if (banner) banner.classList.add('hidden');
const headerBtn = document.getElementById('header-pwa-install-btn');
if (headerBtn) headerBtn.classList.add('hidden');
}}
deferredInstallPrompt = null;
}});
}} else if (isIos) {{
const iosBanner = document.getElementById('ios-install-banner');
if (iosBanner) {{
iosBanner.classList.remove('hidden');
iosBanner.scrollIntoView({{ behavior: 'smooth' }});
}} else {{
alert("Als App nutzen: Im Safari-Menü auf 'Teilen' [↑] tippen und 'Zum Home-Bildschirm' (+) wählen.");
}}
}} else {{
alert("Zum Installieren: Im Browser-Menü auf 'App installieren' oder das Installations-Symbol in der Adressleiste tippen.");
}}
}};
// ==============================================================================
// Self-Updating Engine & Live Tickers
// ==============================================================================