diff --git a/apps/todo/apps/web/src/app.html b/apps/todo/apps/web/src/app.html
index 77a5ff52c..076d6148e 100644
--- a/apps/todo/apps/web/src/app.html
+++ b/apps/todo/apps/web/src/app.html
@@ -1,9 +1,29 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
%sveltekit.head%
diff --git a/apps/todo/apps/web/static/icons/icon.svg b/apps/todo/apps/web/static/icons/icon.svg
new file mode 100644
index 000000000..31ac90c22
--- /dev/null
+++ b/apps/todo/apps/web/static/icons/icon.svg
@@ -0,0 +1,25 @@
+
diff --git a/apps/todo/apps/web/static/manifest.json b/apps/todo/apps/web/static/manifest.json
new file mode 100644
index 000000000..5afff146a
--- /dev/null
+++ b/apps/todo/apps/web/static/manifest.json
@@ -0,0 +1,71 @@
+{
+ "name": "Todo - Aufgabenverwaltung",
+ "short_name": "Todo",
+ "description": "Aufgaben und Projekte verwalten mit Kanban-Board, Subtasks und mehr",
+ "start_url": "/",
+ "scope": "/",
+ "display": "standalone",
+ "background_color": "#ffffff",
+ "theme_color": "#8b5cf6",
+ "orientation": "any",
+ "categories": ["productivity", "utilities"],
+ "lang": "de",
+ "dir": "ltr",
+ "icons": [
+ {
+ "src": "/icons/icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml",
+ "purpose": "any"
+ },
+ {
+ "src": "/icons/icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml",
+ "purpose": "maskable"
+ }
+ ],
+ "shortcuts": [
+ {
+ "name": "Neue Aufgabe",
+ "short_name": "Neu",
+ "description": "Neue Aufgabe erstellen",
+ "url": "/?action=new",
+ "icons": [
+ {
+ "src": "/icons/icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml"
+ }
+ ]
+ },
+ {
+ "name": "Kanban Board",
+ "short_name": "Kanban",
+ "description": "Kanban-Ansicht öffnen",
+ "url": "/kanban",
+ "icons": [
+ {
+ "src": "/icons/icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml"
+ }
+ ]
+ },
+ {
+ "name": "Einstellungen",
+ "short_name": "Settings",
+ "description": "App-Einstellungen öffnen",
+ "url": "/settings",
+ "icons": [
+ {
+ "src": "/icons/icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml"
+ }
+ ]
+ }
+ ],
+ "screenshots": [],
+ "prefer_related_applications": false
+}
diff --git a/apps/todo/apps/web/static/offline.html b/apps/todo/apps/web/static/offline.html
new file mode 100644
index 000000000..7b932041a
--- /dev/null
+++ b/apps/todo/apps/web/static/offline.html
@@ -0,0 +1,228 @@
+
+
+
+
+
+ Offline - Todo
+
+
+
+
+
+
+
Du bist offline
+
Keine Internetverbindung. Sobald du wieder online bist, kannst du deine Aufgaben verwalten.
+
+
+
+ Verbindung wird gesucht...
+
+
+
+
+
+
Was du tun kannst
+
+ - Überprüfe deine WLAN-Verbindung
+ - Prüfe deine mobilen Daten
+ - Versuche es in einigen Sekunden erneut
+
+
+
+
+
+
+
diff --git a/apps/todo/apps/web/static/sw.js b/apps/todo/apps/web/static/sw.js
new file mode 100644
index 000000000..7992ed8ec
--- /dev/null
+++ b/apps/todo/apps/web/static/sw.js
@@ -0,0 +1,154 @@
+const CACHE_NAME = 'todo-v1';
+const OFFLINE_URL = '/offline.html';
+
+// Assets, die immer gecacht werden sollen
+const STATIC_CACHE_URLS = ['/', '/offline.html', '/icons/icon.svg', '/manifest.json'];
+
+// Cache-Strategien für verschiedene Ressourcen
+const CACHE_STRATEGIES = {
+ // Netzwerk zuerst, dann Cache (für HTML/Navigation)
+ networkFirst: [/\/$/, /\.html$/, /^\/kanban/, /^\/settings/, /^\/mana/, /^\/feedback/],
+ // Cache zuerst, dann Netzwerk (für Assets)
+ cacheFirst: [
+ /\.css$/,
+ /\.js$/,
+ /\.woff2?$/,
+ /\.ttf$/,
+ /\.otf$/,
+ /\.svg$/,
+ /\.png$/,
+ /\.jpg$/,
+ /\.jpeg$/,
+ /\.webp$/,
+ /\.ico$/,
+ /\/_app\//,
+ ],
+ // Nur Netzwerk (für API-Calls)
+ networkOnly: [/\/api\//, /localhost:3018/],
+};
+
+// Service Worker Installation
+self.addEventListener('install', (event) => {
+ event.waitUntil(
+ caches
+ .open(CACHE_NAME)
+ .then((cache) => {
+ console.log('Todo Service Worker: Caching static assets');
+ return cache.addAll(STATIC_CACHE_URLS);
+ })
+ .then(() => self.skipWaiting())
+ );
+});
+
+// Service Worker Aktivierung
+self.addEventListener('activate', (event) => {
+ event.waitUntil(
+ caches
+ .keys()
+ .then((cacheNames) => {
+ return Promise.all(
+ cacheNames
+ .filter((cacheName) => cacheName.startsWith('todo-') && cacheName !== CACHE_NAME)
+ .map((cacheName) => caches.delete(cacheName))
+ );
+ })
+ .then(() => self.clients.claim())
+ );
+});
+
+// Fetch-Event Handler
+self.addEventListener('fetch', (event) => {
+ const { request } = event;
+ const url = new URL(request.url);
+
+ // Ignoriere Chrome Extension Requests
+ if (url.protocol === 'chrome-extension:') {
+ return;
+ }
+
+ // Ignoriere Cross-Origin Requests (z.B. Backend API)
+ if (url.origin !== self.location.origin) {
+ return;
+ }
+
+ // Bestimme die Cache-Strategie
+ const strategy = getStrategy(url.pathname);
+
+ if (strategy === 'networkFirst') {
+ event.respondWith(networkFirst(request));
+ } else if (strategy === 'cacheFirst') {
+ event.respondWith(cacheFirst(request));
+ } else if (strategy === 'networkOnly') {
+ event.respondWith(networkOnly(request));
+ } else {
+ // Standard: Network First
+ event.respondWith(networkFirst(request));
+ }
+});
+
+// Cache-Strategien Implementierung
+async function networkFirst(request) {
+ try {
+ const networkResponse = await fetch(request);
+ if (networkResponse.ok) {
+ const cache = await caches.open(CACHE_NAME);
+ cache.put(request, networkResponse.clone());
+ }
+ return networkResponse;
+ } catch (error) {
+ const cachedResponse = await caches.match(request);
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+
+ // Wenn es eine Navigation ist und wir offline sind, zeige die Offline-Seite
+ if (request.mode === 'navigate') {
+ const offlineResponse = await caches.match(OFFLINE_URL);
+ if (offlineResponse) {
+ return offlineResponse;
+ }
+ }
+
+ throw error;
+ }
+}
+
+async function cacheFirst(request) {
+ const cachedResponse = await caches.match(request);
+ if (cachedResponse) {
+ return cachedResponse;
+ }
+
+ try {
+ const networkResponse = await fetch(request);
+ if (networkResponse.ok) {
+ const cache = await caches.open(CACHE_NAME);
+ cache.put(request, networkResponse.clone());
+ }
+ return networkResponse;
+ } catch (error) {
+ console.error('Todo SW: Fetch failed:', error);
+ throw error;
+ }
+}
+
+async function networkOnly(request) {
+ return fetch(request);
+}
+
+// Hilfsfunktion zur Bestimmung der Cache-Strategie
+function getStrategy(pathname) {
+ for (const [strategy, patterns] of Object.entries(CACHE_STRATEGIES)) {
+ if (patterns.some((pattern) => pattern.test(pathname))) {
+ return strategy;
+ }
+ }
+ return 'networkFirst';
+}
+
+// Message Handler für Updates
+self.addEventListener('message', (event) => {
+ if (event.data && event.data.type === 'SKIP_WAITING') {
+ self.skipWaiting();
+ }
+});
diff --git a/docs/PWA_GUIDE.md b/docs/PWA_GUIDE.md
new file mode 100644
index 000000000..20efe719a
--- /dev/null
+++ b/docs/PWA_GUIDE.md
@@ -0,0 +1,792 @@
+# PWA Guide - Progressive Web Apps im Monorepo
+
+Diese Anleitung beschreibt, wie eine bestehende Web-App in eine installierbare PWA (Progressive Web App) umgewandelt wird.
+
+## Übersicht
+
+Eine PWA benötigt folgende Komponenten:
+
+| Komponente | Datei | Zweck |
+|------------|-------|-------|
+| Web App Manifest | `static/manifest.json` | App-Metadaten, Icons, Shortcuts |
+| Service Worker | `static/sw.js` | Offline-Support, Caching |
+| Offline-Seite | `static/offline.html` | Fallback wenn offline |
+| App-Icons | `static/icons/` | Icons für verschiedene Plattformen |
+| Meta-Tags | `src/app.html` | PWA-Konfiguration im HTML |
+| SW Registration | `+layout.svelte` | Service Worker starten |
+
+---
+
+## Schritt 1: App-Icons erstellen
+
+### Verzeichnis anlegen
+
+```
+static/icons/
+├── icon.svg # Basis-Icon (SVG, skalierbar)
+├── icon-72x72.png # Optional: PNG für ältere Browser
+├── icon-96x96.png
+├── icon-128x128.png
+├── icon-144x144.png
+├── icon-152x152.png
+├── icon-192x192.png
+├── icon-384x384.png
+├── icon-512x512.png
+└── apple-touch-icon.png # 180x180 für iOS
+```
+
+### SVG-Icon Vorlage
+
+SVG ist ideal, da es skalierbar ist und nur eine Datei benötigt:
+
+```svg
+
+```
+
+### Farbschema pro App
+
+| App | Primary Color | Gradient |
+|-----|---------------|----------|
+| Todo | `#8b5cf6` | `#8b5cf6` → `#7c3aed` |
+| Chat | `#3b82f6` | `#3b82f6` → `#2563eb` |
+| Picture | `#ec4899` | `#ec4899` → `#db2777` |
+| Zitare | `#f59e0b` | `#f59e0b` → `#d97706` |
+| Calendar | `#10b981` | `#10b981` → `#059669` |
+| Contacts | `#6366f1` | `#6366f1` → `#4f46e5` |
+| Mana Games | `#00ff88` | `#00ff88` → `#00cc6a` |
+
+---
+
+## Schritt 2: Web App Manifest
+
+### Datei: `static/manifest.json`
+
+```json
+{
+ "name": "App Name - Beschreibung",
+ "short_name": "App Name",
+ "description": "Kurze Beschreibung der App",
+ "start_url": "/",
+ "display": "standalone",
+ "background_color": "#ffffff",
+ "theme_color": "#8b5cf6",
+ "orientation": "any",
+ "categories": ["productivity"],
+ "lang": "de",
+ "icons": [
+ {
+ "src": "/icons/icon.svg",
+ "sizes": "any",
+ "type": "image/svg+xml",
+ "purpose": "any"
+ },
+ {
+ "src": "/icons/icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "/icons/icon-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ }
+ ],
+ "shortcuts": [
+ {
+ "name": "Shortcut Name",
+ "short_name": "Kurz",
+ "description": "Beschreibung",
+ "url": "/path?action=shortcut",
+ "icons": [
+ {
+ "src": "/icons/icon-96x96.png",
+ "sizes": "96x96"
+ }
+ ]
+ }
+ ]
+}
+```
+
+### Manifest-Felder erklärt
+
+| Feld | Beschreibung | Beispiel |
+|------|--------------|----------|
+| `name` | Vollständiger App-Name | "Todo - Aufgabenverwaltung" |
+| `short_name` | Kurzname (max 12 Zeichen) | "Todo" |
+| `description` | App-Beschreibung | "Aufgaben verwalten" |
+| `start_url` | Start-URL beim Öffnen | "/" |
+| `display` | Anzeigemodus | "standalone", "fullscreen", "minimal-ui" |
+| `background_color` | Hintergrund beim Laden | "#ffffff" |
+| `theme_color` | Statusleisten-Farbe | "#8b5cf6" |
+| `orientation` | Bildschirmorientierung | "any", "portrait", "landscape" |
+| `categories` | App Store Kategorien | ["productivity", "utilities"] |
+| `lang` | Sprache | "de" |
+| `icons` | App-Icons Array | Siehe oben |
+| `shortcuts` | Quick Actions | Siehe oben |
+
+### Display-Modi
+
+| Modus | Beschreibung |
+|-------|--------------|
+| `fullscreen` | Komplett bildschirmfüllend, keine Browser-UI |
+| `standalone` | Wie native App, mit Statusleiste |
+| `minimal-ui` | Mit minimaler Browser-Navigation |
+| `browser` | Normaler Browser-Tab |
+
+---
+
+## Schritt 3: Service Worker
+
+### Datei: `static/sw.js`
+
+```javascript
+const CACHE_NAME = 'app-name-v1';
+const OFFLINE_URL = '/offline.html';
+
+// Statische Assets die immer gecacht werden
+const STATIC_CACHE_URLS = [
+ '/',
+ '/offline.html',
+ '/icons/icon.svg',
+ '/manifest.json'
+];
+
+// Cache-Strategien für verschiedene Ressourcen
+const CACHE_STRATEGIES = {
+ // Network First: Immer aktuell, Cache als Fallback
+ networkFirst: [
+ /\/$/, // Root
+ /\.html$/, // HTML-Dateien
+ /^\/app/, // App-Routen
+ ],
+
+ // Cache First: Schnell laden, im Hintergrund aktualisieren
+ cacheFirst: [
+ /\.css$/,
+ /\.js$/,
+ /\.woff2?$/,
+ /\.ttf$/,
+ /\.svg$/,
+ /\.png$/,
+ /\.jpg$/,
+ /\.jpeg$/,
+ /\.webp$/,
+ /\/_app\//, // SvelteKit Assets
+ ],
+
+ // Network Only: Nie cachen (API-Calls)
+ networkOnly: [
+ /\/api\//,
+ /localhost:\d+/,
+ ]
+};
+
+// Installation
+self.addEventListener('install', (event) => {
+ event.waitUntil(
+ caches.open(CACHE_NAME)
+ .then((cache) => {
+ console.log('SW: Caching static assets');
+ return cache.addAll(STATIC_CACHE_URLS);
+ })
+ .then(() => self.skipWaiting())
+ );
+});
+
+// Aktivierung (alte Caches löschen)
+self.addEventListener('activate', (event) => {
+ event.waitUntil(
+ caches.keys()
+ .then((cacheNames) => {
+ return Promise.all(
+ cacheNames
+ .filter((name) => name.startsWith('app-name-') && name !== CACHE_NAME)
+ .map((name) => caches.delete(name))
+ );
+ })
+ .then(() => self.clients.claim())
+ );
+});
+
+// Fetch-Handler
+self.addEventListener('fetch', (event) => {
+ const { request } = event;
+ const url = new URL(request.url);
+
+ // Chrome Extensions ignorieren
+ if (url.protocol === 'chrome-extension:') return;
+
+ // Cross-Origin ignorieren
+ if (url.origin !== self.location.origin) return;
+
+ const strategy = getStrategy(url.pathname);
+
+ if (strategy === 'networkFirst') {
+ event.respondWith(networkFirst(request));
+ } else if (strategy === 'cacheFirst') {
+ event.respondWith(cacheFirst(request));
+ } else if (strategy === 'networkOnly') {
+ event.respondWith(fetch(request));
+ } else {
+ event.respondWith(networkFirst(request));
+ }
+});
+
+// Network First Strategy
+async function networkFirst(request) {
+ try {
+ const response = await fetch(request);
+ if (response.ok) {
+ const cache = await caches.open(CACHE_NAME);
+ cache.put(request, response.clone());
+ }
+ return response;
+ } catch (error) {
+ const cached = await caches.match(request);
+ if (cached) return cached;
+
+ // Offline-Seite für Navigation
+ if (request.mode === 'navigate') {
+ const offline = await caches.match(OFFLINE_URL);
+ if (offline) return offline;
+ }
+
+ throw error;
+ }
+}
+
+// Cache First Strategy
+async function cacheFirst(request) {
+ const cached = await caches.match(request);
+ if (cached) return cached;
+
+ try {
+ const response = await fetch(request);
+ if (response.ok) {
+ const cache = await caches.open(CACHE_NAME);
+ cache.put(request, response.clone());
+ }
+ return response;
+ } catch (error) {
+ console.error('SW: Fetch failed:', error);
+ throw error;
+ }
+}
+
+// Strategie ermitteln
+function getStrategy(pathname) {
+ for (const [strategy, patterns] of Object.entries(CACHE_STRATEGIES)) {
+ if (patterns.some((pattern) => pattern.test(pathname))) {
+ return strategy;
+ }
+ }
+ return 'networkFirst';
+}
+
+// Update-Nachricht empfangen
+self.addEventListener('message', (event) => {
+ if (event.data?.type === 'SKIP_WAITING') {
+ self.skipWaiting();
+ }
+});
+```
+
+### Caching-Strategien erklärt
+
+| Strategie | Wann verwenden | Verhalten |
+|-----------|----------------|-----------|
+| **Network First** | HTML, dynamische Inhalte | Versucht Netzwerk, fällt auf Cache zurück |
+| **Cache First** | Statische Assets (CSS, JS, Bilder) | Lädt aus Cache, aktualisiert im Hintergrund |
+| **Network Only** | API-Calls, Echtzeit-Daten | Kein Caching, immer Netzwerk |
+| **Stale While Revalidate** | Häufig aktualisierte Inhalte | Cache sofort, Netzwerk im Hintergrund |
+
+### Cache-Versionierung
+
+Bei Updates den Cache-Namen erhöhen:
+
+```javascript
+// Version erhöhen bei Breaking Changes
+const CACHE_NAME = 'app-name-v2'; // War v1
+```
+
+---
+
+## Schritt 4: Offline-Seite
+
+### Datei: `static/offline.html`
+
+```html
+
+
+
+
+
+ Offline - App Name
+
+
+
+
+
+
+
Du bist offline
+
Keine Internetverbindung. Sobald du wieder online bist, kannst du die App nutzen.
+
+
+
+ Verbindung wird gesucht...
+
+
+
+
+
+
+
+
+```
+
+---
+
+## Schritt 5: PWA Meta-Tags
+
+### Datei: `src/app.html`
+
+Im `` hinzufügen:
+
+```html
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
+```
+
+### Meta-Tags erklärt
+
+| Tag | Zweck |
+|-----|-------|
+| `` | Verlinkt das Web App Manifest |
+| `` | Farbe der Browser-Statusleiste |
+| `apple-mobile-web-app-capable` | Ermöglicht "Add to Home Screen" auf iOS |
+| `apple-mobile-web-app-status-bar-style` | iOS Statusleisten-Stil |
+| `apple-mobile-web-app-title` | App-Name auf iOS Homescreen |
+| `apple-touch-icon` | Icon für iOS Homescreen |
+| `msapplication-TileColor` | Windows Tile Farbe |
+
+---
+
+## Schritt 6: Service Worker Registration
+
+### Datei: `src/routes/(app)/+layout.svelte`
+
+Im `onMount` hinzufügen:
+
+```typescript
+import { onMount } from 'svelte';
+
+onMount(async () => {
+ // ... andere Initialisierungen ...
+
+ // Service Worker registrieren
+ if ('serviceWorker' in navigator) {
+ try {
+ const registration = await navigator.serviceWorker.register('/sw.js', {
+ scope: '/',
+ });
+ console.log('PWA: Service Worker registered', registration.scope);
+
+ // Update-Erkennung
+ registration.addEventListener('updatefound', () => {
+ const newWorker = registration.installing;
+ if (newWorker) {
+ newWorker.addEventListener('statechange', () => {
+ if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
+ // Neue Version verfügbar
+ console.log('PWA: New version available');
+ // Optional: Benutzer informieren
+ showUpdateNotification();
+ }
+ });
+ }
+ });
+ } catch (error) {
+ console.error('PWA: Service Worker registration failed', error);
+ }
+ }
+});
+
+function showUpdateNotification() {
+ // Optional: Toast/Banner anzeigen
+ // "Neue Version verfügbar - Seite neu laden?"
+}
+```
+
+---
+
+## Schritt 7: Optionale Features
+
+### Install-Prompt anzeigen
+
+```typescript
+let deferredPrompt: BeforeInstallPromptEvent | null = null;
+let showInstallButton = $state(false);
+
+onMount(() => {
+ window.addEventListener('beforeinstallprompt', (e) => {
+ e.preventDefault();
+ deferredPrompt = e as BeforeInstallPromptEvent;
+ showInstallButton = true;
+ });
+
+ window.addEventListener('appinstalled', () => {
+ showInstallButton = false;
+ deferredPrompt = null;
+ });
+});
+
+async function installApp() {
+ if (!deferredPrompt) return;
+
+ deferredPrompt.prompt();
+ const { outcome } = await deferredPrompt.userChoice;
+
+ if (outcome === 'accepted') {
+ console.log('PWA installed');
+ }
+
+ deferredPrompt = null;
+ showInstallButton = false;
+}
+```
+
+```svelte
+{#if showInstallButton}
+
+{/if}
+```
+
+### Update-Banner
+
+```svelte
+
+
+{#if showUpdateBanner}
+
+ Neue Version verfügbar
+
+
+{/if}
+```
+
+### Offline-Status anzeigen
+
+```svelte
+
+
+{#if !isOnline}
+
+ Offline
+
+{/if}
+```
+
+---
+
+## Checkliste
+
+### Vor dem Deployment
+
+- [ ] `manifest.json` erstellt mit korrekten Metadaten
+- [ ] App-Icons in allen benötigten Größen vorhanden
+- [ ] `sw.js` erstellt mit passenden Cache-Strategien
+- [ ] `offline.html` erstellt mit App-Branding
+- [ ] PWA Meta-Tags in `app.html` eingefügt
+- [ ] Service Worker Registration im Layout
+- [ ] Theme-Color passt zur App
+- [ ] `start_url` ist korrekt
+- [ ] Shortcuts sind sinnvoll definiert
+
+### Testen
+
+```bash
+# Chrome DevTools
+1. F12 öffnen
+2. Application Tab
+3. "Manifest" prüfen
+4. "Service Workers" prüfen
+5. "Cache Storage" prüfen
+
+# Lighthouse Audit
+1. F12 öffnen
+2. Lighthouse Tab
+3. "Progressive Web App" aktivieren
+4. "Analyze page load" klicken
+```
+
+### PWA-Kriterien (Lighthouse)
+
+| Kriterium | Anforderung |
+|-----------|-------------|
+| Installierbar | Manifest + Service Worker |
+| Offline-fähig | Service Worker mit Caching |
+| HTTPS | Erforderlich (außer localhost) |
+| Responsive | Viewport Meta-Tag |
+| Schnell | First Contentful Paint < 3s |
+
+---
+
+## Referenz-Implementierungen
+
+### Im Monorepo
+
+| App | Verzeichnis |
+|-----|-------------|
+| Mana Games | `games/mana-games/apps/web/public/` |
+| Todo | `apps/todo/apps/web/static/` |
+
+### Externe Ressourcen
+
+- [Web.dev PWA Guide](https://web.dev/progressive-web-apps/)
+- [MDN Web App Manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest)
+- [Workbox (Google's SW Library)](https://developer.chrome.com/docs/workbox/)
+
+---
+
+## Troubleshooting
+
+### Service Worker wird nicht registriert
+
+```
+Fehler: "Service Worker registration failed"
+```
+
+**Lösungen:**
+1. HTTPS verwenden (oder localhost)
+2. Pfad zu `sw.js` prüfen (muss im root sein)
+3. Browser-Cache leeren
+4. DevTools → Application → Service Workers → "Update on reload" aktivieren
+
+### Manifest wird nicht erkannt
+
+```
+Fehler: "No matching service worker detected"
+```
+
+**Lösungen:**
+1. `` im `` prüfen
+2. JSON-Syntax in `manifest.json` validieren
+3. Icons-Pfade prüfen
+
+### Offline-Seite wird nicht angezeigt
+
+**Lösungen:**
+1. `offline.html` ist in `STATIC_CACHE_URLS` enthalten
+2. Service Worker ist aktiviert
+3. Cache-Name stimmt überein
+
+### App lässt sich nicht installieren
+
+**Voraussetzungen:**
+1. HTTPS (oder localhost)
+2. Gültiges Manifest mit `name`, `icons`, `start_url`, `display`
+3. Service Worker registriert
+4. Icon mindestens 192x192px