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
59 lines
2.4 KiB
HTML
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>
|