diff --git a/index.js b/index.js index db7391e..d89440b 100644 --- a/index.js +++ b/index.js @@ -25,6 +25,7 @@ app.use(helmet({ fontSrc: ["'self'", 'https://fonts.gstatic.com'], imgSrc: ["'self'", 'data:'], connectSrc: ["'self'", 'https://api.open-meteo.com'], + workerSrc: ["'self'"], frameAncestors: ["'none'"], objectSrc: ["'none'"], }, @@ -32,6 +33,10 @@ app.use(helmet({ })); app.use(express.json()); app.use(cookieParser()); +app.get('/sw.js', (req, res, next) => { + res.setHeader('Cache-Control', 'no-cache'); + next(); +}); app.use(express.static(path.join(__dirname, 'public'))); app.use('/api', routes); app.use('/api/files', filesRouter); diff --git a/public/admin.html b/public/admin.html index 0620ac8..7042fa6 100644 --- a/public/admin.html +++ b/public/admin.html @@ -3,6 +3,13 @@ + + + + + + + Klassenportal · Admin @@ -122,6 +129,7 @@ tbody td { padding: 10px 14px; vertical-align: middle; } .lucide { display: inline-block; vertical-align: -0.125em; flex-shrink: 0; width: 1em; height: 1em; stroke-width: 2; } .tab .lucide { width: 14px; height: 14px; } + diff --git a/public/app.html b/public/app.html index 771f009..fc55e9e 100644 --- a/public/app.html +++ b/public/app.html @@ -3,6 +3,13 @@ + + + + + + + Klassenportal · Dashboard @@ -1010,6 +1017,7 @@ footer { .btn-sm .lucide { width: 13px; height: 13px; } .sidebar-close .lucide { width: 16px; height: 16px; } + diff --git a/public/datenschutz.html b/public/datenschutz.html index 2b45828..651ca22 100644 --- a/public/datenschutz.html +++ b/public/datenschutz.html @@ -3,6 +3,13 @@ + + + + + + + Datenschutzerklärung · Klassenportal @@ -30,6 +37,7 @@ th { text-align: left; padding: 8px 12px; background: #f8fafc; font-weight: 600; color: #374151; border-bottom: 1px solid #e2e8f0; } td { padding: 8px 12px; color: #334155; border-bottom: 1px solid #f1f5f9; vertical-align: top; } +
diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..a7e97b8 Binary files /dev/null and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..f159411 Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..68a969a Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/public/icons/icon-maskable-192.png b/public/icons/icon-maskable-192.png new file mode 100644 index 0000000..8046e67 Binary files /dev/null and b/public/icons/icon-maskable-192.png differ diff --git a/public/icons/icon-maskable-512.png b/public/icons/icon-maskable-512.png new file mode 100644 index 0000000..5a5adf2 Binary files /dev/null and b/public/icons/icon-maskable-512.png differ diff --git a/public/index.html b/public/index.html index b8fb9f6..17a16d8 100644 --- a/public/index.html +++ b/public/index.html @@ -3,6 +3,13 @@ + + + + + + + Klassenportal · Das Klassen-Cockpit @@ -604,6 +611,7 @@ footer { .brand-sub { display: none; } } + diff --git a/public/login.html b/public/login.html index 23289fd..a22cfa3 100644 --- a/public/login.html +++ b/public/login.html @@ -3,6 +3,13 @@ + + + + + + + Klassenportal · Anmelden @@ -186,6 +193,7 @@ footer a:hover { color: #2563eb; } .lucide { display: inline-block; vertical-align: -0.125em; flex-shrink: 0; width: 1em; height: 1em; stroke-width: 2; } .notice .lucide { width: 14px; height: 14px; margin-right: 3px; } + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..c29e402 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,38 @@ +{ + "name": "Klassenportal", + "short_name": "Klassenportal", + "description": "Privates Klassenportal für Schüler und Lehrer", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "any", + "theme_color": "#2563eb", + "background_color": "#f4f6f9", + "lang": "de", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/icons/icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/public/reset-password.html b/public/reset-password.html index d5d4fd8..055d92d 100644 --- a/public/reset-password.html +++ b/public/reset-password.html @@ -3,6 +3,13 @@ + + + + + + + Klassenportal · Passwort zurücksetzen @@ -64,6 +71,7 @@ h1 { font-size: 18px; font-weight: 700; margin-bottom: 6px; letter-spacing: -.2p footer { text-align: center; padding: 18px; font-size: 12px; color: #9ca3af; } footer a { color: #6b7280; text-decoration: none; } +
diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..eb9d653 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,70 @@ +const CACHE = 'klassenportal-v1'; +const PRECACHE = [ + '/', + '/login.html', + '/app.html', + '/manifest.json', + '/favicon.svg', + '/icons/icon-192.png', + '/icons/icon-512.png', + '/icons/apple-touch-icon.png', +]; + +self.addEventListener('install', e => { + e.waitUntil( + caches.open(CACHE) + .then(c => c.addAll(PRECACHE)) + .then(() => self.skipWaiting()) + ); +}); + +self.addEventListener('activate', e => { + e.waitUntil( + caches.keys() + .then(keys => Promise.all( + keys.filter(k => k !== CACHE).map(k => caches.delete(k)) + )) + .then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', e => { + const url = new URL(e.request.url); + + // Only handle same-origin GET requests + if (url.origin !== self.location.origin) return; + if (e.request.method !== 'GET') return; + + // API calls: always network, never cache + if (url.pathname.startsWith('/api/')) return; + + // Page navigations: network-first, fallback to cache + if (e.request.mode === 'navigate') { + e.respondWith( + fetch(e.request) + .then(r => { + const clone = r.clone(); + caches.open(CACHE).then(c => c.put(e.request, clone)); + return r; + }) + .catch(() => + caches.match(e.request).then(r => r || caches.match('/login.html')) + ) + ); + return; + } + + // Static assets: cache-first, populate on miss + e.respondWith( + caches.match(e.request).then(cached => { + if (cached) return cached; + return fetch(e.request).then(r => { + if (r.ok) { + const clone = r.clone(); + caches.open(CACHE).then(c => c.put(e.request, clone)); + } + return r; + }); + }) + ); +});