From 199a65e2a5d6284e592965abda3fcc93eef1aa80 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Mon, 11 May 2026 07:44:25 +0200 Subject: [PATCH] fix: Caching auf max 2 h reduzieren MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bisher konnten alte JS-/CSS-Dateien durch Service-Worker- und Browser- Cache hartnäckig hängen bleiben, obwohl auf dem Server schon eine neue Version lag. Strategie jetzt: Backend (main.py) - Neue HTTP-Middleware setzt explizite Cache-Control-Header: * /, /index.html, /manifest.json, /sw.js, /static/js/version.js bekommen no-cache, no-store, must-revalidate * /static/* und /icons/* bekommen public, max-age=7200, must-revalidate (2 h) * SPA-Fallback-Antworten ebenfalls no-cache * /api/* bleibt unangetastet Service Worker (sw.js) - Wechsel von Cache-First zu Network-First für alles - Cache wird nur noch für die index.html-Offline-Hülle vorgehalten, nicht mehr für JS/CSS — Browser-HTTP-Cache übernimmt das mit den 2-h-Headern vom Server - Bei Netzwerkfehler bleibt nur die HTML-Shell offline verfügbar Version v11 → v12 (auch SW-Cache-Key). --- backend/main.py | 35 +++++++++++++- frontend/index.html | 20 ++++---- frontend/js/version.js | 2 +- frontend/sw.js | 107 ++++++++++++++--------------------------- 4 files changed, 82 insertions(+), 82 deletions(-) diff --git a/backend/main.py b/backend/main.py index dade943..5225ef6 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,11 +4,16 @@ import sys from pathlib import Path import uvicorn -from fastapi import FastAPI, HTTPException +from fastapi import FastAPI, HTTPException, Request from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from sqlalchemy import text +# How long the browser may keep static assets before revalidating. +STATIC_MAX_AGE_SECONDS = 2 * 60 * 60 # 2 hours +NO_CACHE = "no-cache, no-store, must-revalidate" +STATIC_CACHE = f"public, max-age={STATIC_MAX_AGE_SECONDS}, must-revalidate" + sys.path.insert(0, str(Path(__file__).parent)) from database import Base, engine @@ -113,6 +118,34 @@ _migrate() app = FastAPI(title="Calendarr", docs_url=None, redoc_url=None) + +@app.middleware("http") +async def add_cache_headers(request: Request, call_next): + """Force ≤ 2h browser cache for static assets and disable cache for the + entry HTML / SW / version file. API responses are left alone (handlers + decide their own caching).""" + response = await call_next(request) + path = request.url.path + + # Never cache: entry HTML, manifest, service worker, version marker + if ( + path in ("/", "/index.html", "/manifest.json", "/sw.js") + or path == "/static/js/version.js" + ): + response.headers["Cache-Control"] = NO_CACHE + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "0" + # 2h cache for the rest of the frontend (JS/CSS/icons/etc.) + elif path.startswith("/static/") or path.startswith("/icons/"): + response.headers["Cache-Control"] = STATIC_CACHE + # SPA fallback (everything else that isn't an API route) returns HTML; + # don't let the browser cache that either. + elif not path.startswith("/api/"): + response.headers["Cache-Control"] = NO_CACHE + + return response + + app.include_router(auth_router.router, prefix="/api/auth", tags=["auth"]) app.include_router(users_router.router, prefix="/api/users", tags=["users"]) app.include_router(caldav_router.router, prefix="/api/caldav", tags=["caldav"]) diff --git a/frontend/index.html b/frontend/index.html index 8197413..96a3587 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Calendarr v11 + Calendarr v12 @@ -80,7 +80,7 @@ - + @@ -185,7 +185,7 @@ Meine Kalender
- + @@ -235,7 +235,7 @@
- +
@@ -243,7 +243,7 @@
- +
@@ -253,7 +253,7 @@
- +
@@ -261,7 +261,7 @@
- +
@@ -311,7 +311,7 @@
- +
@@ -884,7 +884,7 @@ scarriffleservices@gmail.com

diff --git a/frontend/js/version.js b/frontend/js/version.js index 3fc6af0..0ad52c0 100644 --- a/frontend/js/version.js +++ b/frontend/js/version.js @@ -1,2 +1,2 @@ // Increment APP_VERSION with every code change -export const APP_VERSION = 'v11'; +export const APP_VERSION = 'v12'; diff --git a/frontend/sw.js b/frontend/sw.js index a76f4fb..e94ffbc 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,39 +1,21 @@ -// Calendarr Service Worker -// Cache-first for static assets, network-first for /api/* (graceful offline) +// Calendarr Service Worker — minimal-cache strategy +// +// Strategy: network-first for everything. The cache is only used as a +// last-resort fallback when offline (so the app shell still opens). This +// means every online request hits the network and respects the +// server's Cache-Control headers (≤ 2h for static assets, no-cache for +// the entry HTML / version files). New releases take effect on the next +// reload, no manual SW unregister required. -const CACHE_VERSION = 'calendarr-v11'; -const STATIC_ASSETS = [ - '/', - '/index.html', - '/manifest.json', - '/static/css/app.css', - '/static/favicon.svg', - '/static/js/app.js', - '/static/js/api.js', - '/static/js/calendar.js', - '/static/js/color-picker.js', - '/static/js/date-picker.js', - '/static/js/i18n.js', - '/static/js/utils.js', - '/static/js/version.js', - '/static/js/views/agenda.js', - '/static/js/views/month.js', - '/static/js/views/quarter.js', - '/static/js/views/week.js', - '/icons/icon-192.png', - '/icons/icon-512.png', - '/icons/icon.svg', -]; +const CACHE_VERSION = 'calendarr-v12'; +const OFFLINE_SHELL = ['/', '/index.html']; self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_VERSION).then(cache => - // Use addAll with a fallback so a single missing file doesn't abort install - Promise.all( - STATIC_ASSETS.map(url => - cache.add(url).catch(err => console.warn('[SW] skip', url, err)) - ) - ) + Promise.all(OFFLINE_SHELL.map(url => + cache.add(url).catch(err => console.warn('[SW] skip', url, err)) + )) ).then(() => self.skipWaiting()) ); }); @@ -52,7 +34,8 @@ self.addEventListener('fetch', event => { const url = new URL(req.url); - // Network-first for API routes — fail silently if offline + // API routes: always go to the network, no offline fallback (we'd just + // be returning stale account/event data otherwise). if (url.pathname.startsWith('/api/')) { event.respondWith( fetch(req).catch(() => @@ -65,45 +48,29 @@ self.addEventListener('fetch', event => { return; } - // Network-first for navigation (HTML) and the version-defining files — - // ensures users always get the freshest entry point so new releases - // take effect on the next reload without a manual SW unregister. - const isHtml = req.mode === 'navigate' - || url.pathname === '/' - || url.pathname === '/index.html'; - const isVersionFile = url.pathname === '/static/js/version.js'; - - if (isHtml || isVersionFile) { - event.respondWith( - fetch(req).then(resp => { - if (resp && resp.status === 200) { - const clone = resp.clone(); - caches.open(CACHE_VERSION).then(c => c.put(req, clone)).catch(() => {}); - } - return resp; - }).catch(() => - caches.match(req).then(c => c || caches.match('/index.html')) - ) - ); - return; - } - - // Cache-first for everything else (static) + // Everything else: network-first. The browser's HTTP cache (driven by + // the server's Cache-Control headers) already throttles re-fetches — + // the SW just makes sure offline still works for the entry HTML. event.respondWith( - caches.match(req).then(cached => { - if (cached) return cached; - return fetch(req).then(resp => { - // Only cache successful, basic-origin responses - if (resp && resp.status === 200 && resp.type === 'basic') { - const clone = resp.clone(); - caches.open(CACHE_VERSION).then(c => c.put(req, clone)).catch(() => {}); - } - return resp; - }).catch(() => { - // Offline fallback for navigation requests - if (req.mode === 'navigate') return caches.match('/index.html'); - return new Response('', { status: 503 }); - }); + fetch(req).then(resp => { + // Keep a fresh copy of navigation requests / index.html for offline + const isNavigation = req.mode === 'navigate' + || url.pathname === '/' + || url.pathname === '/index.html'; + if (isNavigation && resp && resp.status === 200) { + const clone = resp.clone(); + caches.open(CACHE_VERSION).then(c => c.put(req, clone)).catch(() => {}); + } + return resp; + }).catch(() => { + // Offline fallback: only the HTML shell is served from cache, so the + // app at least renders and can show its own offline UI. + if (req.mode === 'navigate' + || url.pathname === '/' + || url.pathname === '/index.html') { + return caches.match(req).then(c => c || caches.match('/index.html')); + } + return new Response('', { status: 503 }); }) ); });