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
@@ -199,7 +199,7 @@
-
+
@@ -235,7 +235,7 @@
@@ -253,7 +253,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 });
})
);
});