feat(pwa): add auto-update on reopen, live tickers, and manual refresh to dashboards

This commit is contained in:
Moritz Graf
2026-10-11 22:25:37 +02:00
parent 63497765b0
commit e9d2c59eed
3 changed files with 225 additions and 9 deletions
@@ -537,6 +537,8 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="build-time" content="{now_berlin.isoformat()}">
<meta name="build-epoch" content="{int(now_berlin.timestamp())}">
<title>Moritz Health — Energiebilanz & Sport</title>
<!-- PWA & Mobile Web App Meta Tags -->
@@ -587,6 +589,9 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
</div>
</div>
<div class="flex items-center gap-2">
<button id="manual-refresh-btn" onclick="triggerManualRefresh()" title="Aktualisieren" class="inline-flex items-center justify-center p-2 rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-600 hover:text-slate-900 border border-slate-200/80 transition active:scale-95 shadow-xs">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
</button>
<button onclick="openModal('modal-today-meals')" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-sky-50 text-sky-700 hover:bg-sky-100 border border-sky-200/70 text-xs font-semibold shadow-xs transition">
<i data-lucide="utensils" class="w-3.5 h-3.5"></i>
<span>Heute ({today_count})</span>
@@ -1106,6 +1111,91 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
document.addEventListener('DOMContentLoaded', () => {{
lucide.createIcons();
}});
// Service Worker Registration for PWA caching & offline support
if ('serviceWorker' in navigator) {{
window.addEventListener('load', () => {{
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));
}});
}}
// ==============================================================================
// Self-Updating Engine
// ==============================================================================
(function() {{
const currentBuildEpoch = parseInt(document.querySelector('meta[name="build-epoch"]')?.content || '0', 10);
let lastCheckMs = Date.now();
let isChecking = false;
// 1. Server Update Checker
async function checkForUpdates() {{
if (isChecking) return;
isChecking = true;
try {{
const res = await fetch(window.location.pathname + '?_poll=' + Date.now(), {{
cache: 'no-store',
headers: {{ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache' }}
}});
if (res.ok) {{
const html = await res.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const remoteEpoch = parseInt(doc.querySelector('meta[name="build-epoch"]')?.content || '0', 10);
if (remoteEpoch && currentBuildEpoch && remoteEpoch > currentBuildEpoch) {{
console.log('[AutoUpdate] Server has newer build (' + remoteEpoch + ' > ' + currentBuildEpoch + '). Reloading...');
window.location.reload();
return;
}}
}}
}} catch (err) {{
console.warn('[AutoUpdate] Check failed:', err);
}} finally {{
isChecking = false;
lastCheckMs = Date.now();
}}
}}
// 2. Auto-update on reopen / foreground resume (PWA & Mobile Safari)
document.addEventListener('visibilitychange', () => {{
if (document.visibilityState === 'visible') {{
if (Date.now() - lastCheckMs > 10000) {{
checkForUpdates();
}}
}}
}});
window.addEventListener('pageshow', (e) => {{
if (e.persisted || Date.now() - lastCheckMs > 10000) {{
checkForUpdates();
}}
}});
window.addEventListener('focus', () => {{
if (Date.now() - lastCheckMs > 15000) {{
checkForUpdates();
}}
}});
// 3. Background check every 30 seconds when visible
setInterval(() => {{
if (document.visibilityState === 'visible') {{
checkForUpdates();
}}
}}, 30000);
// 4. Manual refresh trigger
window.triggerManualRefresh = function() {{
const btn = document.getElementById('manual-refresh-btn');
if (btn) {{
const icon = btn.querySelector('svg') || btn.querySelector('i');
if (icon) icon.classList.add('animate-spin');
}}
window.location.reload();
}};
}})();
</script>
</body>
</html>