evOo-local/apps/web/index.html
Claude a163447461
feat(notifications): add push subscriptions, notification settings and web push interfaces
Push could only reach one mobile device per user (a token file), quiet hours did not exist,
and the web app had no service worker, so a closed tab never heard about finished work.

This lands the shared interfaces first so onboarding can build on them:
- push_subscriptions (web push endpoints and Expo tokens, one row per device) and
  user_notification_settings (quiet hours as minutes in the profile time zone, nudges, a
  per-day nudge counter), with CHECK constraints.
- NotificationSettingsSchema and web push contracts, notifications.settings.get/update and
  notifications.webPush.config/subscribe/unsubscribe (owner-scoped, https endpoints only).
- getNotificationSettings/updateNotificationSettings plus the policy and nudge-cap helpers.
- VAPID keys from VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY or generated once and stored encrypted,
  and an SSRF-guarded web push sender built on the web-push package.
- The web app links its manifest, registers a push-only service worker where supported (secure
  context, not the desktop app) and exposes webPushSupported/webPushEnabled/enableWebPush/
  disableWebPush.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 13:13:22 +00:00

59 lines
2.4 KiB
HTML

<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Deine Oos — immer da, immer wach." />
<meta name="theme-color" content="#07080a" />
<meta name="apple-mobile-web-app-title" content="evOo" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<!-- Fonts loaded locally via @fontsource-variable (Inter + Geist) — no external CDN -->
<link rel="stylesheet" href="/evoo-brand.css" />
<title>evOo</title>
<script>
(() => {
const key = "rakazo.uiAppearance";
let preference = "system";
try {
const stored = localStorage.getItem(key);
if (stored === "system" || stored === "light" || stored === "dark") preference = stored;
} catch {}
const systemLight =
typeof matchMedia === "function" && matchMedia("(prefers-color-scheme: light)").matches;
const theme = preference === "system" ? (systemLight ? "light" : "dark") : preference;
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", theme === "light" ? "#fafaf8" : "#07080a");
})();
</script>
<script>
// Reload once when the browser aborts part of the module graph while the app
// boots (a network interface changed mid-request). main.tsx clears the flag
// after a successful boot; a second failure in a row is left to the browser.
addEventListener(
"error",
(event) => {
const target = event.target;
if (!(target instanceof HTMLScriptElement) || target.type !== "module") return;
try {
if (sessionStorage.getItem("rakazo:boot-reload")) return;
sessionStorage.setItem("rakazo:boot-reload", "1");
} catch {
return;
}
location.reload();
},
true,
);
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>