diff --git a/apps/mana/apps/web/src/routes/(app)/+layout.svelte b/apps/mana/apps/web/src/routes/(app)/+layout.svelte index e8fe6b9a0..a16cc5984 100644 --- a/apps/mana/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/mana/apps/web/src/routes/(app)/+layout.svelte @@ -3,7 +3,7 @@ import { page } from '$app/stores'; import type { Snippet } from 'svelte'; import { onDestroy, setContext } from 'svelte'; - import { createReminderScheduler, notificationService } from '@mana/shared-stores'; + import { createReminderScheduler } from '@mana/shared-stores'; import { todoReminderSource } from '$lib/modules/todo/reminder-source'; import KeyboardShortcutsModal from '$lib/components/KeyboardShortcutsModal.svelte'; import SessionWarning from '$lib/components/SessionWarning.svelte'; @@ -332,7 +332,20 @@ // Phase B2: Start reminder scheduler reminderScheduler.start(); - notificationService.requestPermission(); + // IMPORTANT: do NOT call notificationService.requestPermission() here. + // Browsers (Chrome/Firefox) require permission requests to come from + // a user gesture. Calling it at mount time queues the prompt until + // the next click, which means the FIRST click on any button (e.g. + // the dreams "Traum sprechen" mic button) shows a notification + // permission popup instead of the action the user actually clicked + // — and getUserMedia() / other permission requests get silently + // dropped because Chrome only shows one permission dialog at a time. + // + // Notification permission must be requested from a button the user + // explicitly clicks ("Benachrichtigungen aktivieren" toggle in + // Settings, or first time a reminder is created). The reminder + // scheduler still runs without permission — it just won't fire + // OS notifications until the user grants it. // Phase C: Guest mode — welcome modal + nudge if (!authStore.isAuthenticated) { diff --git a/apps/mana/apps/web/vite.config.ts b/apps/mana/apps/web/vite.config.ts index 510b5ad16..815874a96 100644 --- a/apps/mana/apps/web/vite.config.ts +++ b/apps/mana/apps/web/vite.config.ts @@ -20,6 +20,15 @@ export default defineConfig({ themeColor: '#6366f1', registerType: 'prompt', preset: 'full', + // Disable the service worker in dev. With devEnabled=true (the + // default) vite-plugin-pwa registers a SW that aggressively + // precaches the route chunks — and after the first dev session + // the SW keeps serving the OLD JS even when Vite HMR pushes + // new code, so source edits become invisible until the user + // manually unregisters the worker in DevTools. The 2026-04-08 + // dreams mic-button bug took an extra hour to track down for + // exactly this reason. Production still gets the full SW. + devEnabled: false, shortcuts: [ { name: 'Dashboard', short_name: 'Home', url: '/', description: 'Zum Dashboard' }, { diff --git a/packages/shared-utils/src/security-headers.ts b/packages/shared-utils/src/security-headers.ts index e2eed020c..4209e9993 100644 --- a/packages/shared-utils/src/security-headers.ts +++ b/packages/shared-utils/src/security-headers.ts @@ -49,7 +49,14 @@ export function setSecurityHeaders(response: Response, options: SecurityHeadersO response.headers.set('X-Frame-Options', 'DENY'); response.headers.set('X-Content-Type-Options', 'nosniff'); response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin'); - response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=(self)'); + // Permissions-Policy: allow microphone for `self` so dreams/memoro voice + // capture (getUserMedia) works on mana.how. `microphone=()` would block + // the API entirely — Chrome reports `[Violation] Permissions policy + // violation: microphone is not allowed in this document` and the + // permission dialog never appears, even if the user has explicitly + // granted access in OS + browser settings. Camera stays disallowed + // since no module needs it. + response.headers.set('Permissions-Policy', 'camera=(), microphone=(self), geolocation=(self)'); // Content Security Policy const cspDirectives = [