diff --git a/app/main.py b/app/main.py index dc02445..be594c3 100755 --- a/app/main.py +++ b/app/main.py @@ -10,7 +10,7 @@ from typing import List, Optional, Dict, Any from contextlib import asynccontextmanager from fastapi import FastAPI, HTTPException, Request, Query, Form, File, UploadFile, WebSocket, WebSocketDisconnect -from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse +from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse, FileResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from datetime import datetime, date, timedelta @@ -70,7 +70,7 @@ APP_USERNAME = os.getenv("APP_USERNAME", "admin") APP_PASSWORD = os.getenv("APP_PASSWORD", "wtc2026!") SECRET_KEY = os.getenv("SECRET_KEY", "wtc_cookbook_secret_salt_2026_supersecure") COOKIE_NAME = "wtc_auth" -PUBLIC_PATHS = {"/login", "/api/login", "/favicon.ico"} +PUBLIC_PATHS = {"/login", "/api/login", "/favicon.ico", "/manifest.json", "/sw.js"} def create_auth_token(username: str, days: int = 30) -> str: expiry = int(time.time()) + (days * 86400) @@ -224,6 +224,25 @@ class ShoppingItemCategoryUpdate(BaseModel): category: str +# ========================================== +# PWA MANIFEST & SERVICE WORKER +# ========================================== + +@app.get("/manifest.json") +def get_manifest(): + manifest_path = os.path.join(STATIC_DIR, "manifest.json") + if os.path.exists(manifest_path): + return FileResponse(manifest_path, media_type="application/manifest+json") + raise HTTPException(status_code=404, detail="Manifest not found") + +@app.get("/sw.js") +def get_service_worker(): + sw_path = os.path.join(STATIC_DIR, "sw.js") + if os.path.exists(sw_path): + return FileResponse(sw_path, media_type="application/javascript", headers={"Service-Worker-Allowed": "/"}) + raise HTTPException(status_code=404, detail="Service worker not found") + + # ========================================== # AUTH VIEWS # ========================================== diff --git a/app/static/manifest.json b/app/static/manifest.json index bf9a115..f25c31c 100755 --- a/app/static/manifest.json +++ b/app/static/manifest.json @@ -1,21 +1,36 @@ { - "name": "What To Cook? – Essens-Zufallsgenerator", - "short_name": "WhatToCook", - "description": "Zufallsgenerator für Essen mit Kategorien und Aufwandsfilter", + "name": "What To Cook? – Kochbuch & Einkaufsliste", + "short_name": "What To Cook?", + "description": "Familien-Kochbuch, Essensplaner und smarte Einkaufsliste", "start_url": "/", "display": "standalone", - "background_color": "#0f141c", - "theme_color": "#ff6b35", + "orientation": "portrait-primary", + "background_color": "#FAF6F0", + "theme_color": "#1E4D3B", "icons": [ { "src": "/static/icon-192.png", "sizes": "192x192", - "type": "image/png" + "type": "image/png", + "purpose": "any maskable" }, { "src": "/static/icon-512.png", "sizes": "512x512", - "type": "image/png" + "type": "image/png", + "purpose": "any maskable" + } + ], + "shortcuts": [ + { + "name": "Einkaufsliste", + "url": "/shopping-list", + "description": "Direkt zur Einkaufsliste" + }, + { + "name": "Kochbuch", + "url": "/cookbook", + "description": "Direkt zum Kochbuch" } ] } diff --git a/app/static/sw.js b/app/static/sw.js new file mode 100644 index 0000000..2022210 --- /dev/null +++ b/app/static/sw.js @@ -0,0 +1,100 @@ +// What To Cook? - PWA Service Worker (v1.0.0) +const CACHE_NAME = 'wtc-cache-v1'; +const PRECACHE_URLS = [ + '/', + '/cookbook', + '/shopping-list', + '/manifest.json', + '/static/cookbook.css', + '/static/icon-192.png', + '/static/icon-512.png' +]; + +// Install: Pre-cache shell assets +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(PRECACHE_URLS).catch((err) => { + console.warn('[SW] Pre-caching partial failure, continuing:', err); + }); + }).then(() => self.skipWaiting()) + ); +}); + +// Activate: Clean up old caches +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((cacheName) => { + if (cacheName !== CACHE_NAME) { + return caches.delete(cacheName); + } + }) + ); + }).then(() => self.clients.claim()) + ); +}); + +// Fetch: Strategy +self.addEventListener('fetch', (event) => { + const req = event.request; + const url = new URL(req.url); + + // Skip non-GET and WebSocket/API mutation requests + if (req.method !== 'GET') return; + if (url.pathname.startsWith('/ws/') || url.pathname.startsWith('/api/')) return; + + // Navigation requests (HTML pages): Network first, fallback to cache + if (req.mode === 'navigate') { + event.respondWith( + fetch(req).then((response) => { + if (response && response.status === 200) { + const copy = response.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)); + } + return response; + }).catch(async () => { + const cached = await caches.match(req); + if (cached) return cached; + // If exact page not cached, return home or shopping list if available + if (url.pathname === '/shopping-list') { + const shopCached = await caches.match('/shopping-list'); + if (shopCached) return shopCached; + } + const homeCached = await caches.match('/'); + if (homeCached) return homeCached; + return new Response('Offline – keine Verbindung vorhanden.', { + status: 503, + headers: { 'Content-Type': 'text/plain; charset=utf-8' } + }); + }) + ); + return; + } + + // Static assets: Cache-first, then network update + if (url.pathname.startsWith('/static/') || url.hostname.includes('fonts.googleapis.com') || url.hostname.includes('fonts.gstatic.com')) { + event.respondWith( + caches.match(req).then((cached) => { + if (cached) { + // In background, refresh cache + fetch(req).then((networkRes) => { + if (networkRes && networkRes.status === 200) { + caches.open(CACHE_NAME).then((cache) => cache.put(req, networkRes)); + } + }).catch(() => {}); + return cached; + } + return fetch(req).then((networkRes) => { + if (networkRes && networkRes.status === 200) { + const copy = networkRes.clone(); + caches.open(CACHE_NAME).then((cache) => cache.put(req, copy)); + } + return networkRes; + }); + }) + ); + return; + } +}); diff --git a/app/templates/cookbook.html b/app/templates/cookbook.html index 8bdc684..b92b800 100755 --- a/app/templates/cookbook.html +++ b/app/templates/cookbook.html @@ -4,7 +4,13 @@