Progressive Web App (PWA)
Patrimoine is an installable PWA with offline support for cached static assets. The service worker is built by vite-plugin-pwa (workbox generateSW strategy, configured in frontend/vite.config.ts’s VitePWA() plugin call), with security-first caching to prevent cross-user data leakage on shared devices.
For the user-facing side (installing, offline use, updates) see Install as an app.
Manifest
Section titled “Manifest”The web app manifest is generated by vite-plugin-pwa from the manifest option in vite.config.ts and emitted as manifest.json (manifestFilename: 'manifest.json') — there is no hand-written public/manifest.json.
| Property | Value |
|---|---|
| App name | Patrimoine |
| Display | Standalone |
| Icons | favicon.svg, icon-192.png, icon-512.png (any); icon-maskable-192.png, icon-maskable-512.png (maskable) |
| Theme color | #0e0f12 (same value as index.html’s <meta name="theme-color">) |
| Background color | #0e0f12 |
| Orientation | Not set (any orientation) — deliberate: the layout is responsive, and standalone windows on desktop and tablet must be free to rotate or resize |
Service Worker
Section titled “Service Worker”The service worker is generated at build time by vite-plugin-pwa’s workbox generateSW strategy — there is no hand-written public/sw.js. It is registered from frontend/src/components/PwaUpdatePrompt.tsx via registerSW() from the virtual module virtual:pwa-register, called with { immediate: true } inside a useEffect.
Registration failures are handled by an onRegisterError callback that only logs to the console in development mode (import.meta.env.DEV); in production, failures are silently caught to avoid polluting logs.
vite.config.ts sets registerType: 'prompt' (not the default 'autoUpdate').
'prompt' instead calls an onNeedRefresh callback and never reloads on its own.
Cache Strategies
Section titled “Cache Strategies”| Resource | Strategy | Rationale |
|---|---|---|
Static assets (precached via workbox.globPatterns: ['**/*.{js,css,html,svg,png,ico,woff2}'], minus globIgnores: assets/pdf/** and the en, es, de and it catalogues under assets/locales/) |
Cache-first (precache + cleanupOutdatedCaches()) |
Static assets are content-hashed; cached versions are safe to serve. French stays precached as the default language and the offline fallback |
PDF export chunks (assets/pdf/) and the en, es, de and it catalogues (assets/locales/) |
Runtime cache-first in the lazy-chunks cache, 16 entries, only 200 responses |
Fetched and stored the first time they are used instead of at install; safe because the names are content-hashed; cleared by the logout purge like every other cache |
Navigation requests (/, deep links) |
Served from the precached index.html via a workbox NavigationRoute, except paths under /api/, /auth/ and /health (see navigateFallbackDenylist in vite.config.ts) |
Keeps client-side routing working offline while never intercepting API/auth calls; the health probe must always answer from the backend |
API calls (/api/*), auth calls (/auth/*) and the health check (/health) |
Excluded from the SW entirely (denylisted, network-only) | Prevents cross-user data leakage on shared devices — API/auth responses are never cached or served from the SW |
The /health entry is the anchored pattern /^\/health(?:[/?]|$)/: it matches /health, /health/ and /health?x, but not a future SPA route that merely starts with “health”.
Cache Purge on Logout
Section titled “Cache Purge on Logout”There is no CLEAR_CACHES service-worker message. Cache purging on logout is done directly by the app in frontend/src/hooks/useLogout.ts: it calls caches.keys() and caches.delete() on every Cache Storage entry (when 'caches' in window) before POSTing to /auth/logout and redirecting to /. This ensures no sensitive data remains in Cache Storage on shared devices.
skipWaiting / clients.claim
Section titled “skipWaiting / clients.claim”The generated service worker listens for a SKIP_WAITING message and calls self.skipWaiting() in response — but only in response. Nothing calls clientsClaim(). In practice this means:
- The new service worker installs and waits, but does not take control of open pages on its own.
PwaUpdatePrompt.tsxis the only caller ofupdateSW()(the functionregisterSW()returns), and it only callsupdateSW(true)when the user clicks “reload” on the update toast shown by itsonNeedRefreshhandler — see Update Flow below.- Because
clientsClaim()is never called, a newly activated worker still doesn’t take over already-open tabs outside of that explicit user action; the user always sees a consistent version of the app rather than a half-updated state mid-session.
Update Flow
Section titled “Update Flow”registerSW({ immediate: true, onNeedRefresh, onRegisterError })registers the SW on app load.- When workbox detects a new SW version,
onNeedRefreshfires.PwaUpdatePrompt.tsxshows a persistentsonnertoast (duration: Infinity) with a “reload” action button. - Only if the user clicks that button does the app call
updateSW(true), which sendsSKIP_WAITINGto the waiting worker and reloads the page once it activates.
Theme Initialization
Section titled “Theme Initialization”The theme initialization script is at public/theme-init.js. It is loaded as an external script (not inline) to comply with Content Security Policy (CSP).
Before React hydrates, the script reads the user’s skin and mode preferences from localStorage ('skin' and 'mode' keys) and sets two independent attributes on <html>:
data-skin(one offintech,azur,noir,swiss,aurora,ledger)data-mode(resolved tolightordark—systemis resolved againstwindow.matchMedia('(prefers-color-scheme: dark)')before it’s written, sodata-modenever holds the literal string'system').
This prevents a flash of the wrong theme (FOUC) on page load.
See Design System — Skins and Modes for the full mechanism (this script is a hand-kept mirror of useSkin.ts’s valid-value lists, kept in sync by a regression test).
Color Scheme and Theme Color
Section titled “Color Scheme and Theme Color”index.html includes:
<meta name="color-scheme" content="light dark">— lets the browser natively style form controls and scrollbars- A single
<meta name="theme-color" content="#0e0f12">
The manifest uses the same value for theme_color and background_color, so the splash screen, the standalone title bar and the browser chrome agree. It is not synced to the active skin: a light skin keeps a dark title bar in the installed app.
| File | Description |
|---|---|
public/favicon.svg |
Rising area chart design; also used as the any-purpose manifest icon |
public/icon-192.png |
192px PWA icon (any purpose) |
public/icon-512.png |
512px PWA icon (any purpose) |
public/icon-maskable-192.png |
192px maskable PWA icon: full-bleed #0e0f12, no transparency, glyph inside the safe zone |
public/icon-maskable-512.png |
512px maskable PWA icon: full-bleed #0e0f12, no transparency, glyph inside the safe zone |
public/apple-touch-icon.png |
180px opaque iOS Home Screen icon, linked from index.html; iOS prefers it over the manifest icons |
public/icons.svg |
Icon sprite |
The last three PNGs are rendered once from frontend/scripts/icon-maskable.svg by sh frontend/scripts/generate-icons.sh (needs rsvg-convert) and committed; the build does not generate them.
Offline Behavior
Section titled “Offline Behavior”- Cached static assets (HTML, CSS, JS, icons) are available offline via the workbox precache
- The PDF export and a language other than French are available offline only after their first use online (they are stored in the
lazy-chunksruntime cache, not in the precache) - API calls require a network connection — no offline data access (denylisted from the SW)
- Previously viewed data may be available if the app shell was cached, but dynamic data is not persisted offline
Service Worker Cache Versioning
Section titled “Service Worker Cache Versioning”There is no manually-maintained version string to bump.
vite-plugin-pwa’s workbox generateSW build generates a fresh precache manifest (with a content hash per asset, revision set accordingly) on every production build, and cleanupOutdatedCaches() removes stale precache entries automatically when a new SW activates — so cache invalidation is automatic per deploy rather than driven by a hand-bumped version.