Harmonize navigation and modals across cookbook & shopping list, fix WebSocket proxying, and add PWA offline support

This commit is contained in:
2026-10-15 21:03:15 +02:00
parent 55b2823113
commit d59629cc6e
6 changed files with 1371 additions and 1427 deletions
+21 -2
View File
@@ -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
# ==========================================
+22 -7
View File
@@ -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"
}
]
}
+100
View File
@@ -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;
}
});
+13 -1
View File
@@ -4,7 +4,13 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mein Kochbuch – What To Cook?</title>
<link rel="stylesheet" href="/static/cookbook.css?v=4.5.4">
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1E4D3B">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="What To Cook?">
<link rel="apple-touch-icon" href="/static/icon-192.png">
<link rel="stylesheet" href="/static/cookbook.css?v=4.6.1">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📖</text></svg>">
<style id="print-engine-styles">
/* Auf dem Bildschirm: Cover, Inhaltsverzeichnis und Seitenzahlen ausblenden */
@@ -2717,6 +2723,12 @@
openRecipeModal();
}
});
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(err => console.log('SW register fail:', err));
});
}
</script>
</body>
</html>
+13
View File
@@ -4,6 +4,12 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>What To Cook? – Menü</title>
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1E4D3B">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="What To Cook?">
<link rel="apple-touch-icon" href="/static/icon-192.png">
<link rel="stylesheet" href="/static/cookbook.css?v=4.6.1">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍽️</text></svg>">
<style>
@@ -289,5 +295,12 @@
</main>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(err => console.log('SW register fail:', err));
});
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff