diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index 8739d1a..7501cf3 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -36,6 +36,19 @@ server { try_files $uri $uri/ /index.php?$query_string; } + # PWA: correct manifest MIME, and keep the service worker un-cached so updates ship. + location = /manifest.webmanifest { + types { } + default_type application/manifest+json; + add_header Cache-Control "public, max-age=3600"; + access_log off; + } + + location = /sw.js { + add_header Cache-Control "no-cache"; + add_header Service-Worker-Allowed "/"; + } + location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..5fcda22 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..1c1adbf Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..5b39a49 Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/public/icons/icon-maskable-192.png b/public/icons/icon-maskable-192.png new file mode 100644 index 0000000..e2e49ec Binary files /dev/null and b/public/icons/icon-maskable-192.png differ diff --git a/public/icons/icon-maskable-512.png b/public/icons/icon-maskable-512.png new file mode 100644 index 0000000..613fe07 Binary files /dev/null and b/public/icons/icon-maskable-512.png differ diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..6e99d59 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,56 @@ +{ + "name": "HomeOS", + "short_name": "HomeOS", + "description": "Selbst-gehostete Haussteuerung — Räume, Geräte, Automationen.", + "id": "/", + "start_url": "/dashboard", + "scope": "/", + "display": "standalone", + "display_override": ["standalone", "minimal-ui"], + "orientation": "any", + "background_color": "#080D18", + "theme_color": "#0D1424", + "lang": "de", + "dir": "ltr", + "categories": ["utilities", "lifestyle"], + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "shortcuts": [ + { + "name": "Steuerung", + "short_name": "Steuerung", + "url": "/panel", + "icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }] + }, + { + "name": "Räume", + "short_name": "Räume", + "url": "/rooms", + "icons": [{ "src": "/icons/icon-192.png", "sizes": "192x192" }] + } + ] +} diff --git a/public/offline.html b/public/offline.html new file mode 100644 index 0000000..02b70aa --- /dev/null +++ b/public/offline.html @@ -0,0 +1,50 @@ + + + + + + Offline · HomeOS + + + +
+
+ + + + + + +
+

Keine Verbindung

+

HomeOS ist gerade nicht erreichbar. Prüfe deine Netzwerkverbindung zum Server.

+ +
+ + diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..edc756a --- /dev/null +++ b/public/sw.js @@ -0,0 +1,74 @@ +/** + * HomeOS service worker. Deliberately conservative for an authenticated Livewire app: + * - never intercepts non-GET (login, Livewire updates, broadcasting/auth stay online-only) + * - cache-first only for fingerprinted, immutable assets under /build/ and static icons + * - navigations are network-first with an offline fallback, so authenticated HTML is never + * served stale from cache + * Bump VERSION to invalidate old caches on the next activate. + */ +const VERSION = 'homeos-v1'; +const STATIC_CACHE = `${VERSION}-static`; +const OFFLINE_URL = '/offline.html'; +const PRECACHE = [OFFLINE_URL, '/icons/icon-192.png', '/icons/icon-512.png']; + +self.addEventListener('install', (event) => { + event.waitUntil( + (async () => { + const cache = await caches.open(STATIC_CACHE); + await cache.addAll(PRECACHE); + await self.skipWaiting(); + })(), + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + (async () => { + const keys = await caches.keys(); + await Promise.all(keys.filter((k) => !k.startsWith(VERSION)).map((k) => caches.delete(k))); + await self.clients.claim(); + })(), + ); +}); + +self.addEventListener('message', (event) => { + if (event.data === 'skipWaiting') self.skipWaiting(); +}); + +self.addEventListener('fetch', (event) => { + const req = event.request; + if (req.method !== 'GET') return; // login / Livewire / broadcasting are POST — stay online + + const url = new URL(req.url); + if (url.origin !== self.location.origin) return; // don't touch cross-origin + + // Immutable, fingerprinted build output + static icons → cache-first. + if (url.pathname.startsWith('/build/') || url.pathname.startsWith('/icons/')) { + event.respondWith(cacheFirst(req)); + return; + } + + // Page loads → network-first, fall back to the offline shell only when truly offline. + if (req.mode === 'navigate') { + event.respondWith(networkFirst(req)); + } +}); + +async function cacheFirst(req) { + const cache = await caches.open(STATIC_CACHE); + const hit = await cache.match(req); + if (hit) return hit; + const res = await fetch(req); + if (res && res.ok) cache.put(req, res.clone()); + return res; +} + +async function networkFirst(req) { + try { + return await fetch(req); + } catch (e) { + const cache = await caches.open(STATIC_CACHE); + const offline = await cache.match(OFFLINE_URL); + return offline || Response.error(); + } +} diff --git a/resources/js/app.js b/resources/js/app.js index 8337e44..f02bd0f 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -85,3 +85,11 @@ window.Echo = new Echo({ forceTLS: isHttps, enabledTransports: ['ws', 'wss'], }); + +// Register the PWA service worker so HomeOS installs on tablets/phones and survives +// brief server blips (offline shell). sw.js lives at the web root for a "/" scope. +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js').catch(() => {}); + }); +} diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index d0c3329..bab9047 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -5,6 +5,17 @@ + + + {{-- PWA: installable on tablet/phone (§ tablet control) --}} + + + + + + + + {{ $title ?? __('common.app_name') }} · {{ __('common.app_name') }} @vite(['resources/css/app.css', 'resources/js/app.js']) @livewireStyles diff --git a/resources/views/layouts/guest.blade.php b/resources/views/layouts/guest.blade.php index 0f1251f..e7b80df 100644 --- a/resources/views/layouts/guest.blade.php +++ b/resources/views/layouts/guest.blade.php @@ -5,6 +5,11 @@ + + + + + {{ __('auth.title') }} · {{ __('common.app_name') }} @vite(['resources/css/app.css', 'resources/js/app.js']) @livewireStyles