fix(pwa): enable crossorigin=use-credentials for manifest, add id and header install trigger
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"id": "/family/mila.html",
|
||||||
"name": "Mila — Schlaf & Entwicklung",
|
"name": "Mila — Schlaf & Entwicklung",
|
||||||
"short_name": "Mila",
|
"short_name": "Mila",
|
||||||
"description": "Schlaf-, Trink- und Windeltagebuch für Mila Graf",
|
"description": "Schlaf-, Trink- und Windeltagebuch für Mila Graf",
|
||||||
@@ -13,13 +14,25 @@
|
|||||||
"src": "/family/mila-icon-192.png",
|
"src": "/family/mila-icon-192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"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",
|
"src": "/family/mila-icon-512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"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",
|
"src": "/family/mila-apple-touch-icon.png",
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"id": "/family/moritz.html",
|
||||||
|
"name": "Moritz Health",
|
||||||
|
"short_name": "Health",
|
||||||
|
"description": "Persönliches Dashboard für Kalorienbilanz, Ernährung und Garmin-Fitness von Moritz.",
|
||||||
|
"start_url": "/family/moritz.html",
|
||||||
|
"scope": "/family/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"background_color": "#f8fafc",
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><rect width='512' height='512' rx='128' fill='%2310b981'/><path d='M128 256h64l32-96 64 192 32-96h64' fill='none' stroke='white' stroke-width='40' stroke-linecap='round' stroke-linejoin='round'/></svg>",
|
||||||
|
"sizes": "192x192 512x512",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -82,8 +82,8 @@ def get_s3_client():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def publish_to_s3(html_content: str, key: str = "moritz.html") -> str:
|
def publish_to_s3(html_content: str, key: str = "family/moritz.html", data_dir: Optional[Path] = None) -> str:
|
||||||
"""Uploads the rendered HTML to MinIO hosting bucket."""
|
"""Uploads the rendered HTML and webmanifest to MinIO hosting bucket."""
|
||||||
s3 = get_s3_client()
|
s3 = get_s3_client()
|
||||||
s3.put_object(
|
s3.put_object(
|
||||||
Bucket=DEFAULT_BUCKET,
|
Bucket=DEFAULT_BUCKET,
|
||||||
@@ -92,6 +92,22 @@ def publish_to_s3(html_content: str, key: str = "moritz.html") -> str:
|
|||||||
ContentType="text/html; charset=utf-8",
|
ContentType="text/html; charset=utf-8",
|
||||||
CacheControl="no-cache, no-store, must-revalidate",
|
CacheControl="no-cache, no-store, must-revalidate",
|
||||||
)
|
)
|
||||||
|
# Also upload moritz.webmanifest if present
|
||||||
|
assets_dir = SCRIPT_DIR.parent / "assets"
|
||||||
|
manifest_file = assets_dir / "moritz.webmanifest"
|
||||||
|
if not manifest_file.is_file() and data_dir:
|
||||||
|
manifest_file = data_dir.parent / "public" / "family" / "moritz.webmanifest"
|
||||||
|
if manifest_file.is_file():
|
||||||
|
try:
|
||||||
|
s3.put_object(
|
||||||
|
Bucket=DEFAULT_BUCKET,
|
||||||
|
Key="family/moritz.webmanifest",
|
||||||
|
Body=manifest_file.read_bytes(),
|
||||||
|
ContentType="application/manifest+json; charset=utf-8",
|
||||||
|
CacheControl="no-cache, no-store, must-revalidate",
|
||||||
|
)
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"Warning: could not upload family/moritz.webmanifest: {ex}", file=sys.stderr)
|
||||||
return f"{DEFAULT_BASE_URL}/{key}"
|
return f"{DEFAULT_BASE_URL}/{key}"
|
||||||
|
|
||||||
|
|
||||||
@@ -546,7 +562,7 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||||
<meta name="apple-mobile-web-app-title" content="Moritz Health">
|
<meta name="apple-mobile-web-app-title" content="Moritz Health">
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
<link rel="manifest" href="{manifest_data_uri}">
|
<link rel="manifest" href="/family/moritz.webmanifest" crossorigin="use-credentials">
|
||||||
<link rel="apple-touch-icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><rect width='512' height='512' rx='128' fill='%2310b981'/><path d='M128 256h64l32-96 64 192 32-96h64' fill='none' stroke='white' stroke-width='40' stroke-linecap='round' stroke-linejoin='round'/></svg>">
|
<link rel="apple-touch-icon" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><rect width='512' height='512' rx='128' fill='%2310b981'/><path d='M128 256h64l32-96 64 192 32-96h64' fill='none' stroke='white' stroke-width='40' stroke-linecap='round' stroke-linejoin='round'/></svg>">
|
||||||
|
|
||||||
<!-- Tailwind CSS & Lucide Icons -->
|
<!-- Tailwind CSS & Lucide Icons -->
|
||||||
@@ -1231,10 +1247,10 @@ def main():
|
|||||||
# 2. Upload strictly to protected S3 hosting under /family/
|
# 2. Upload strictly to protected S3 hosting under /family/
|
||||||
if not args.skip_upload:
|
if not args.skip_upload:
|
||||||
try:
|
try:
|
||||||
url = publish_to_s3(html, key="family/moritz.html")
|
url = publish_to_s3(html, key="family/moritz.html", data_dir=data_dir)
|
||||||
print(f"🚀 Published successfully to: {url}")
|
print(f"🚀 Published successfully to: {url}")
|
||||||
# Also publish to /family/nutrition.html for backwards compatibility
|
# Also publish to /family/nutrition.html for backwards compatibility
|
||||||
publish_to_s3(html, key="family/nutrition.html")
|
publish_to_s3(html, key="family/nutrition.html", data_dir=data_dir)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ S3 upload failed (local testing?): {e}", file=sys.stderr)
|
print(f"⚠️ S3 upload failed (local testing?): {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
|||||||
@@ -464,9 +464,9 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
|
|||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# PWA Manifest (RFC-compliant base64 data URI + external webmanifest)
|
# PWA Manifest
|
||||||
import base64
|
|
||||||
manifest_dict = {
|
manifest_dict = {
|
||||||
|
"id": "/family/mila.html",
|
||||||
"name": "Mila — Schlaf & Entwicklung",
|
"name": "Mila — Schlaf & Entwicklung",
|
||||||
"short_name": "Mila",
|
"short_name": "Mila",
|
||||||
"description": "Schlaf-, Trink- und Windeltagebuch für Mila Graf",
|
"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",
|
"src": "/family/mila-icon-192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"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",
|
"src": "/family/mila-icon-512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"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",
|
"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
|
# Full HTML Document
|
||||||
html = f"""<!DOCTYPE html>
|
html = f"""<!DOCTYPE html>
|
||||||
@@ -523,8 +533,7 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
|
|||||||
<meta name="theme-color" content="#090A0F">
|
<meta name="theme-color" content="#090A0F">
|
||||||
<meta name="msapplication-TileColor" content="#090A0F">
|
<meta name="msapplication-TileColor" content="#090A0F">
|
||||||
|
|
||||||
<link rel="manifest" href="/family/mila.webmanifest">
|
<link rel="manifest" href="/family/mila.webmanifest" crossorigin="use-credentials">
|
||||||
<link rel="manifest" href="{manifest_uri}">
|
|
||||||
<link rel="apple-touch-icon" href="/family/mila-apple-touch-icon.png">
|
<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="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">
|
<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>
|
||||||
</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="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>'}
|
{'<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">
|
<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>
|
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -775,44 +788,54 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
|
|||||||
lucide.createIcons();
|
lucide.createIcons();
|
||||||
}});
|
}});
|
||||||
|
|
||||||
// Service Worker Registration for PWA installability
|
// Service Worker Registration for PWA installability & offline support
|
||||||
if ('serviceWorker' in navigator) {{
|
if ('serviceWorker' in navigator) {{
|
||||||
window.addEventListener('load', () => {{
|
window.addEventListener('load', () => {{
|
||||||
navigator.serviceWorker.register('/family/sw.js').catch(() => {{
|
navigator.serviceWorker.register('/family/sw.js', {{ scope: '/family/' }})
|
||||||
navigator.serviceWorker.register('sw.js').catch(console.error);
|
.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;
|
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) => {{
|
window.addEventListener('beforeinstallprompt', (e) => {{
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deferredInstallPrompt = e;
|
deferredInstallPrompt = e;
|
||||||
const banner = document.getElementById('pwa-install-banner');
|
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');
|
const installBtn = document.getElementById('pwa-install-btn');
|
||||||
if (installBtn) {{
|
if (installBtn) {{
|
||||||
installBtn.addEventListener('click', async () => {{
|
installBtn.addEventListener('click', () => {{
|
||||||
if (!deferredInstallPrompt) return;
|
triggerPwaInstall();
|
||||||
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;
|
|
||||||
}});
|
}});
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// iOS Safari Detection & Guidance Banner
|
// iOS Safari Detection & Guidance Banner
|
||||||
const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
|
if (isIos && !isStandalone) {{
|
||||||
const isStandalone = window.navigator.standalone === true || window.matchMedia('(display-mode: standalone)').matches;
|
const headerBtn = document.getElementById('header-pwa-install-btn');
|
||||||
if (isIos && !isStandalone && !localStorage.getItem('dismissed_ios_mila_pwa')) {{
|
if (headerBtn) headerBtn.classList.remove('hidden');
|
||||||
const iosBanner = document.getElementById('ios-install-banner');
|
|
||||||
if (iosBanner) iosBanner.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() {{
|
function dismissIosBanner() {{
|
||||||
@@ -821,6 +844,31 @@ def generate_dashboard_html(engine: DatastoreEngine) -> str:
|
|||||||
if (iosBanner) iosBanner.classList.add('hidden');
|
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
|
// Self-Updating Engine & Live Tickers
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user