feat(pwa): installierbar mit manifest, icons und offline-katalog

Manifest, Icons (auch maskable und Apple-Touch) und ein Service Worker.
Auf dem Handy laesst sich Bricktrack damit auf den Startbildschirm legen
und startet ohne Browserleiste; die Verknuepfungen springen direkt zu
"Fehlende Sets" und zur Sammlung.

Der Service Worker liefert Seiten nach dem Muster "erst Netz, dann
Cache": mit Verbindung ist alles aktuell, ohne Verbindung bleibt lesbar,
was schon einmal geladen war - genau der Katalog, den man im Keller oder
im Laden braucht. Statische Build-Dateien und Set-Bilder kommen aus dem
Cache, weil ihre Namen ohnehin unveraenderlich sind.

Bewusst nie aus dem Cache: /api/auth, /api/export und der Healthcheck,
und nichts ausser GET - eine offline "reservierte" Wunschliste waere
eine Luege. Beim Abmelden raeumt die App den Cache, damit auf einem
geteilten Geraet keine lesbaren Seiten zurueckbleiben.

Icons entstehen reproduzierbar aus einer SVG-Quelle
(node scripts/generate-icons.mjs).

Refs Phase 2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
unknown
2026-09-07 07:27:12 +02:00
co-authored by Claude Opus 5
parent 45aa8f13cf
commit 39271f8af7
14 changed files with 329 additions and 1 deletions
+38
View File
@@ -0,0 +1,38 @@
'use client';
import { useEffect } from 'react';
/**
* Registers the service worker.
*
* Only in production: in development the worker would serve stale chunks and
* turn every edit into a puzzle.
*/
export function ServiceWorkerRegistration() {
useEffect(() => {
if (process.env.NODE_ENV !== 'production') return;
if (!('serviceWorker' in navigator)) return;
const register = () => {
void navigator.serviceWorker.register('/sw.js').catch((error) => {
console.error('[pwa] Service Worker konnte nicht registriert werden', error);
});
};
// Waiting for load keeps the worker from competing with the first render.
if (document.readyState === 'complete') register();
else window.addEventListener('load', register, { once: true });
return () => window.removeEventListener('load', register);
}, []);
return null;
}
/** Drops every cached page - called on sign-out. */
export async function clearOfflineCaches(): Promise<void> {
if (!('serviceWorker' in navigator)) return;
const registration = await navigator.serviceWorker.getRegistration();
registration?.active?.postMessage('bricktrack:clear-cache');
}
+3
View File
@@ -3,6 +3,7 @@
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { LogOut, Settings, UserRound } from 'lucide-react';
import { clearOfflineCaches } from '@/components/layout/service-worker';
import { authClient } from '@/lib/auth-client';
import { Button } from '@/components/ui/button';
import {
@@ -20,6 +21,8 @@ export function UserMenu({ user }: { user: SessionUser }) {
async function signOut() {
await authClient.signOut();
// Cached pages must not stay readable on a shared device.
await clearOfflineCaches();
router.push('/anmelden');
router.refresh();
}