diff --git a/skills/datastore/assets/sw.js b/skills/datastore/assets/sw.js index 903d71f..b06bb5a 100644 --- a/skills/datastore/assets/sw.js +++ b/skills/datastore/assets/sw.js @@ -1,5 +1,5 @@ -// Minimal service worker for Mila PWA installability & offline resilience -const CACHE_NAME = 'mila-v1'; +// Minimal service worker for Family PWAs (Mila & Moritz) with instant cache-bypassing navigation +const CACHE_NAME = 'family-pwa-v2'; self.addEventListener('install', (event) => { self.skipWaiting(); @@ -19,7 +19,16 @@ self.addEventListener('activate', (event) => { self.addEventListener('fetch', (event) => { if (event.request.method !== 'GET') return; - // Network-first strategy: Always fetch live real-time updates from MinIO + + // For HTML documents/navigations: ALWAYS bypass HTTP cache to get latest live server rendering + if (event.request.destination === 'document' || event.request.mode === 'navigate') { + event.respondWith( + fetch(event.request, { cache: 'no-cache' }).catch(() => caches.match(event.request)) + ); + return; + } + + // Assets (images, fonts, scripts): network-first with offline cache fallback event.respondWith( fetch(event.request).catch(() => caches.match(event.request)) ); diff --git a/skills/datastore/scripts/update_moritz_website.py b/skills/datastore/scripts/update_moritz_website.py index 6aec716..20ac5bb 100644 --- a/skills/datastore/scripts/update_moritz_website.py +++ b/skills/datastore/scripts/update_moritz_website.py @@ -537,6 +537,8 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: + + Moritz Health — Energiebilanz & Sport @@ -587,6 +589,9 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
+
-
{dur_str}
+
{dur_display}
""") else: @@ -505,6 +510,8 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: + + Mila — Schlaf & Entwicklung @@ -606,9 +613,12 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: - -
- {'
Schl\u00e4ft (' + active_sleep_duration_str + ')
' if is_sleeping else '
Wach (' + wake_window_str + ')
'} + +
+ {'
Schl\u00e4ft (' + active_sleep_duration_str + ')
' if is_sleeping else '
Wach (' + wake_window_str + ')
'} +
@@ -625,7 +635,7 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: {'💤 Status: Schläft gerade' if is_sleeping else '☀️ Status: Wach & Munter'}
- {f'Seit {active_sleep_start_time} Uhr ({active_sleep_duration_str})' if is_sleeping else f'Wachfenster: {wake_window_str}'} + {f'Seit {active_sleep_start_time} Uhr ({active_sleep_duration_str})' if is_sleeping else f'Wachfenster: {wake_window_str}'}

{f'Eingeschlafen im Bett. Schlafintervall l\u00e4uft aktuell.' if is_sleeping else f'Letztes Schläfchen beendet um {last_wake_time_str} Uhr.'} @@ -754,7 +764,7 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:

@@ -808,6 +818,113 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str: const iosBanner = document.getElementById('ios-install-banner'); if (iosBanner) iosBanner.classList.add('hidden'); }} + + // ============================================================================== + // Self-Updating Engine & Live Tickers + // ============================================================================== + (function() {{ + const currentBuildEpoch = parseInt(document.querySelector('meta[name="build-epoch"]')?.content || '0', 10); + let lastCheckMs = Date.now(); + let isChecking = false; + + // 1. Live Running Sleep/Wake Timer (increments live in DOM every 5 seconds) + function tickLiveTimers() {{ + document.querySelectorAll('.live-sleep-timer').forEach(el => {{ + const iso = el.dataset.startIso; + if (!iso) return; + const startMs = new Date(iso).getTime(); + if (isNaN(startMs)) return; + const diffMs = Math.max(0, Date.now() - startMs); + const mins = Math.floor(diffMs / 60000); + const h = Math.floor(mins / 60); + const m = mins % 60; + el.textContent = h > 0 ? (h + ' h ' + String(m).padStart(2, '0') + ' min') : (m + ' min'); + }}); + document.querySelectorAll('.live-wake-timer').forEach(el => {{ + const iso = el.dataset.endIso; + if (!iso) return; + const endMs = new Date(iso).getTime(); + if (isNaN(endMs)) return; + const diffMs = Math.max(0, Date.now() - endMs); + const mins = Math.floor(diffMs / 60000); + const h = Math.floor(mins / 60); + const m = mins % 60; + el.textContent = h > 0 ? (h + ' h ' + String(m).padStart(2, '0') + ' min') : (m + ' min'); + }}); + }} + setInterval(tickLiveTimers, 5000); + tickLiveTimers(); + + // 2. 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(); + }} + }} + + // 3. Auto-update on reopen / foreground resume (PWA & Mobile Safari) + document.addEventListener('visibilitychange', () => {{ + if (document.visibilityState === 'visible') {{ + tickLiveTimers(); + if (Date.now() - lastCheckMs > 10000) {{ + checkForUpdates(); + }} + }} + }}); + + window.addEventListener('pageshow', (e) => {{ + tickLiveTimers(); + if (e.persisted || Date.now() - lastCheckMs > 10000) {{ + checkForUpdates(); + }} + }}); + + window.addEventListener('focus', () => {{ + tickLiveTimers(); + if (Date.now() - lastCheckMs > 15000) {{ + checkForUpdates(); + }} + }}); + + // 4. Background check every 30 seconds when visible + setInterval(() => {{ + if (document.visibilityState === 'visible') {{ + checkForUpdates(); + }} + }}, 30000); + + // 5. 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(); + }}; + }})();