diff --git a/apps/pwa/index.html b/apps/pwa/index.html
index e3cb868..5eca05e 100644
--- a/apps/pwa/index.html
+++ b/apps/pwa/index.html
@@ -4,6 +4,10 @@
+
+
+
+
FamilySync
diff --git a/apps/pwa/public/apple-touch-icon.png b/apps/pwa/public/apple-touch-icon.png
new file mode 100644
index 0000000..69ebb44
Binary files /dev/null and b/apps/pwa/public/apple-touch-icon.png differ
diff --git a/apps/pwa/public/icon-192.png b/apps/pwa/public/icon-192.png
new file mode 100644
index 0000000..ce6e1c8
Binary files /dev/null and b/apps/pwa/public/icon-192.png differ
diff --git a/apps/pwa/public/icon-512.png b/apps/pwa/public/icon-512.png
new file mode 100644
index 0000000..d7a82ef
Binary files /dev/null and b/apps/pwa/public/icon-512.png differ
diff --git a/apps/pwa/vite.config.ts b/apps/pwa/vite.config.ts
index 0035c59..1a53218 100644
--- a/apps/pwa/vite.config.ts
+++ b/apps/pwa/vite.config.ts
@@ -1,8 +1,43 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
- plugins: [react()],
+ plugins: [
+ react(),
+ VitePWA({
+ registerType: 'autoUpdate',
+ // ⚠️ CRITICAL: exclude /callback from SW navigation handling (Gate 2 — T-03-20)
+ // The OIDC authorization-code exchange lands on /callback — if the SW
+ // intercepts this as a navigation, it serves the cached shell instead of
+ // letting the server process the authorization code exchange (login loop).
+ workbox: {
+ navigateFallback: '/index.html',
+ navigateFallbackDenylist: [
+ /^\/callback/, // OIDC redirect endpoint — must reach the server
+ /^\/api\//, // API calls — never serve from cache
+ /^\/health/, // Health endpoint
+ ],
+ // No /api caching — runtimeCaching: [] means all API calls fall through to network
+ runtimeCaching: [],
+ },
+ manifest: {
+ name: 'FamilySync',
+ short_name: 'FamilySync',
+ description: 'Family calendar and lists',
+ theme_color: '#4A90D9',
+ background_color: '#ffffff',
+ display: 'standalone',
+ scope: '/',
+ start_url: '/',
+ icons: [
+ { src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
+ { src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
+ { src: '/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
+ ],
+ },
+ }),
+ ],
server: {
proxy: {
'/health': 'http://localhost:3000',