diff --git a/.gitignore b/.gitignore index bc15c90..feb5611 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ frontend/dist/ # mai worker state .m/ + +# Playwright MCP scratch (screenshots + console logs from local verification) +/.playwright-mcp/ +/paliad-*.png diff --git a/frontend/build.ts b/frontend/build.ts index 8844d50..87041f4 100644 --- a/frontend/build.ts +++ b/frontend/build.ts @@ -41,6 +41,10 @@ async function build() { // Bundle client-side JS const result = await Bun.build({ entrypoints: [ + // app.ts is loaded on every page (SW registration + bottom-nav init + + // install prompt). Keep it ahead of per-page bundles so name collisions + // surface fast. + join(import.meta.dir, "src/client/app.ts"), join(import.meta.dir, "src/client/index.ts"), join(import.meta.dir, "src/client/login.ts"), join(import.meta.dir, "src/client/kostenrechner.ts"), @@ -91,6 +95,15 @@ async function build() { join(DIST, "assets/global.css"), ); + // Copy public/ → dist/ (manifest.json, sw.js, icons/) — served at the + // application root so the service worker can claim scope=/ and so the + // manifest is reachable at /manifest.json without a sub-path rewrite. + await cp( + join(import.meta.dir, "public"), + DIST, + { recursive: true }, + ); + // Render HTML pages await Bun.write(join(DIST, "index.html"), renderIndex()); await Bun.write(join(DIST, "login.html"), renderLogin("login.js")); diff --git a/frontend/icons-src/icon-maskable.svg b/frontend/icons-src/icon-maskable.svg new file mode 100644 index 0000000..811fb09 --- /dev/null +++ b/frontend/icons-src/icon-maskable.svg @@ -0,0 +1,10 @@ + + + + p + diff --git a/frontend/icons-src/icon.svg b/frontend/icons-src/icon.svg new file mode 100644 index 0000000..93788d4 --- /dev/null +++ b/frontend/icons-src/icon.svg @@ -0,0 +1,10 @@ + + + + p + diff --git a/frontend/public/icons/apple-touch-icon.png b/frontend/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..6667e9c Binary files /dev/null and b/frontend/public/icons/apple-touch-icon.png differ diff --git a/frontend/public/icons/favicon-32.png b/frontend/public/icons/favicon-32.png new file mode 100644 index 0000000..3adb6d0 Binary files /dev/null and b/frontend/public/icons/favicon-32.png differ diff --git a/frontend/public/icons/icon-192.png b/frontend/public/icons/icon-192.png new file mode 100644 index 0000000..024abf0 Binary files /dev/null and b/frontend/public/icons/icon-192.png differ diff --git a/frontend/public/icons/icon-512.png b/frontend/public/icons/icon-512.png new file mode 100644 index 0000000..69bc03e Binary files /dev/null and b/frontend/public/icons/icon-512.png differ diff --git a/frontend/public/icons/icon-maskable-192.png b/frontend/public/icons/icon-maskable-192.png new file mode 100644 index 0000000..569cef3 Binary files /dev/null and b/frontend/public/icons/icon-maskable-192.png differ diff --git a/frontend/public/icons/icon-maskable-512.png b/frontend/public/icons/icon-maskable-512.png new file mode 100644 index 0000000..12da5ba Binary files /dev/null and b/frontend/public/icons/icon-maskable-512.png differ diff --git a/frontend/public/manifest.json b/frontend/public/manifest.json new file mode 100644 index 0000000..9f181ca --- /dev/null +++ b/frontend/public/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "Paliad", + "short_name": "Paliad", + "description": "Patentwissen und Aktenverwaltung für das HLC-Patent-Team.", + "start_url": "/dashboard", + "scope": "/", + "display": "standalone", + "orientation": "portrait", + "theme_color": "#65a30d", + "background_color": "#ffffff", + "lang": "de", + "dir": "ltr", + "id": "paliad", + "categories": ["productivity", "business"], + "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/frontend/public/sw.js b/frontend/public/sw.js new file mode 100644 index 0000000..7735cbb --- /dev/null +++ b/frontend/public/sw.js @@ -0,0 +1,70 @@ +// Paliad service worker — minimal cache-first for /assets/* and /icons/*, +// network-first for /api/*, network passthrough for everything else. +// Bumping CACHE_VERSION purges the previous cache on activation. +const CACHE_VERSION = "paliad-v1"; +const STATIC_CACHE = `${CACHE_VERSION}-static`; + +self.addEventListener("install", (event) => { + // Activate the new SW as soon as it is installed; matters when + // CACHE_VERSION changes so users don't keep stale assets. + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + (async () => { + const keys = await caches.keys(); + await Promise.all( + keys + .filter((k) => k !== STATIC_CACHE && k.startsWith("paliad-")) + .map((k) => caches.delete(k)), + ); + await self.clients.claim(); + })(), + ); +}); + +self.addEventListener("fetch", (event) => { + const req = event.request; + if (req.method !== "GET") return; + + const url = new URL(req.url); + if (url.origin !== self.location.origin) return; + + if (url.pathname.startsWith("/assets/") || url.pathname.startsWith("/icons/")) { + event.respondWith(cacheFirst(req)); + return; + } + + if (url.pathname.startsWith("/api/")) { + event.respondWith(networkFirst(req)); + return; + } + + // HTML navigations + everything else: pass through to the network. +}); + +async function cacheFirst(req) { + const cache = await caches.open(STATIC_CACHE); + const cached = await cache.match(req); + if (cached) return cached; + try { + const res = await fetch(req); + if (res && res.ok) cache.put(req, res.clone()); + return res; + } catch (err) { + if (cached) return cached; + throw err; + } +} + +async function networkFirst(req) { + try { + return await fetch(req); + } catch (err) { + const cache = await caches.open(STATIC_CACHE); + const cached = await cache.match(req); + if (cached) return cached; + throw err; + } +} diff --git a/frontend/src/agenda.tsx b/frontend/src/agenda.tsx index b006f61..3ff170a 100644 --- a/frontend/src/agenda.tsx +++ b/frontend/src/agenda.tsx @@ -2,6 +2,7 @@ import { h } from "./jsx"; import { Sidebar } from "./components/Sidebar"; import { BottomNav } from "./components/BottomNav"; import { Footer } from "./components/Footer"; +import { PWAHead } from "./components/PWAHead"; // The /*__PALIAD_AGENDA_DATA__*/ token is replaced at request time by the Go // handler (internal/handlers/agenda_shell.go) with a JSON payload assigned @@ -17,6 +18,7 @@ export function renderAgenda(): string { + Agenda — Paliad + + ); +} diff --git a/frontend/src/courts.tsx b/frontend/src/courts.tsx index 8dd6e60..45eda3e 100644 --- a/frontend/src/courts.tsx +++ b/frontend/src/courts.tsx @@ -2,6 +2,7 @@ import { h } from "./jsx"; import { Sidebar } from "./components/Sidebar"; import { BottomNav } from "./components/BottomNav"; import { Footer } from "./components/Footer"; +import { PWAHead } from "./components/PWAHead"; export function renderCourts(): string { return "" + ( @@ -12,6 +13,7 @@ export function renderCourts(): string { + Gerichtsverzeichnis — Paliad diff --git a/frontend/src/dashboard.tsx b/frontend/src/dashboard.tsx index f3d71b2..70ec1e1 100644 --- a/frontend/src/dashboard.tsx +++ b/frontend/src/dashboard.tsx @@ -2,6 +2,7 @@ import { h } from "./jsx"; import { Sidebar } from "./components/Sidebar"; import { BottomNav } from "./components/BottomNav"; import { Footer } from "./components/Footer"; +import { PWAHead } from "./components/PWAHead"; // The /* __PALIAD_DASHBOARD_DATA__ */ token below is replaced at request time // by the Go handler (internal/handlers/dashboard_shell.go) with a JSON blob @@ -19,6 +20,7 @@ export function renderDashboard(): string { + Dashboard — Paliad