# Notification Settings & Configuration System - Deep Dive
## 1. Settings UI
The notification settings live in the **App Settings modal** under the "Notifications" tab. The modal is opened via `openAppSettings()` at line 9234 of `src/web/public/app.js`, and the HTML structure is in `src/web/public/index.html` starting at line 974.
### Settings Tab Layout (5 tabs total)
The App Settings modal has tabs: Display, Claude CLI, Models, Paths, **Notifications**. The Notifications tab contains:
#### Master Control Section
| Setting | Element ID | Type | Default (Desktop) | Default (Mobile) |
4.**If event type NOT found** (fallback for unknown categories):
- Check legacy `muteCritical`/`muteWarning`/`muteInfo` based on urgency
-`shouldBrowserNotify` = global browser toggle AND (critical/warning OR tab hidden)
-`shouldAudioAlert` = critical urgency AND global audio toggle
### CRITICAL BUG: Category Key Mismatch
The `eventTypes` keys in the preferences schema do NOT match the `category` values used in actual `notify()` calls. This means **per-event-type settings have no effect for most notification categories**:
| eventTypes Key | Actual category Used in notify() | Match? |
**Impact**: When `notify()` receives `category: 'hook-permission'`, it looks up `eventTypes['hook-permission']`, finds nothing, and falls through to the legacy urgency-based logic. The per-event toggles in the settings UI are effectively non-functional for all hook-based and most other notifications.
The only categories that have a chance of matching are those used in subagent notification code, which would need separate verification.
Additional uncategorized notifications that always fall through to urgency-based logic:
2. Calls `savePreferences()` to persist to localStorage (line 9460)
3. Calls `applyHeaderVisibilitySettings()` which hides/shows the notification bell icon (line 9647-9657)
### Bell icon visibility
The notification bell icon in the header (`btn-notifications`) is hidden when `preferences.enabled` is `false` (line 9649-9651). If notifications are disabled while the drawer is open, the drawer is force-closed (line 9654-9657).
## 4. Default Values
### Desktop defaults
| Setting | Default | Source |
|---------|---------|--------|
| enabled | `true` | `loadPreferences()` line 913 |
| browserNotifications | `true` | line 914, negated `isMobile` |
Desktop defaults enable notifications but keep audio off.
### Server-side sync behavior
When loading from server, display settings (which include panel visibility, tracking toggles, etc.) are filtered out to avoid overwriting mobile-specific defaults (lines 9796-9806). Notification prefs from the server only apply if the device has no local prefs yet (line 9816).
### Mobile CSS adjustments
`mobile.css` line 1050 makes the notification drawer full-width on mobile:
```css
.notification-drawer{
width:100%;
max-width:100%;
right:0;
border-radius:0;
padding-left:var(--safe-area-left);
padding-right:var(--safe-area-right);
padding-bottom:var(--safe-area-bottom);
}
```
### Device type detection
`MobileDetection.getDeviceType()` (line 153) uses a simple width check:
-`< 430px` = mobile
-`430-768px` = tablet (treated as desktop for settings keys)
-`>= 768px` = desktop
Note: Only `mobile` vs non-mobile matters for settings keys. Tablet uses the desktop key.
## 6. Notification Permission Flow
### Auto-request on first notification
When `sendBrowserNotif()` is called and `Notification.permission === 'default'` (never asked), the app **auto-requests permission** (line 1110-1118):
Browser notifications require HTTPS for remote access. The settings UI includes a hint: _"For remote access, HTTPS is required. Start with: `codeman web --https`"_. On localhost, HTTP works fine.
The global `audioAlerts` toggle (default: `false`) controls whether audio can play at all. Per-event `audio` toggles further refine which events produce sound.
### Audio generation
Audio is generated via the Web Audio API (line 1163-1181), NOT via audio file playback:
3. For known event types: `eventTypes[category].audio` = true
4. For unknown categories (fallback): urgency must be `'critical'`
### AudioContext lazy initialization
The `AudioContext` is created lazily on first use (line 1166-1168). This is important because browsers require a user gesture before creating an AudioContext. The first audio alert may silently fail if no user interaction has occurred.
### Does it actually work?
Yes, given the prerequisites above are met. However, due to the category key mismatch (Section 3), per-event audio settings are mostly non-functional. The fallback logic means audio only plays for `critical` urgency notifications when the category is unrecognized (which is most of them).
## 8. Per-Session vs Global Settings
### Global only
Notification preferences are **strictly global**. There is no per-session notification configuration.
- Preferences are loaded once from localStorage (line 878)
- All sessions share the same notification rules
### Per-session data in notifications
While settings are global, each notification carries `sessionId` and `sessionName` for:
- Displaying which session triggered the notification (session chip in drawer items)
- Click-to-switch: clicking a notification selects that session tab (line 1206-1208)
- Browser notification onclick: focuses the window and selects the session (line 1134-1139)
### Stuck detection is per-session
The idle/stuck detection timer is per-session (using `this.idleTimers` Map at line 2383), but the threshold comes from the global `stuckThresholdMs` setting. Respawn-enabled sessions are excluded from stuck detection (line 2381).
### Case settings (separate system)
There is a separate "case settings" system (`caseSettings_<caseName>` in localStorage, lines 14513-14520) but it does not include notification preferences.
## 9. Notification Layers (4-layer system)
The notification system operates in 4 independent layers:
| 4. Audio | Web Audio API beep | Conditional | `preferences.audioAlerts` + per-event `audio` |
### Rate limiting
Browser notifications are rate-limited to 1 per 3 seconds (line 1124, `BROWSER_NOTIF_RATE_LIMIT_MS`).
### Notification grouping
Same-category notifications for the same session within 5 seconds are grouped (count incremented) instead of creating new entries (line 986-996, `GROUPING_TIMEOUT_MS`).
### Auto-close
Browser notifications auto-close after 8 seconds (line 1143, `AUTO_CLOSE_NOTIFICATION_MS`).
### List cap
The in-app notification list is capped at 100 entries (line 1014, FIFO eviction).
The per-event settings in the UI are effectively non-functional because the category strings used in `notify()` calls (`hook-permission`, `hook-idle`, `session-error`, etc.) do not match the `eventTypes` keys in preferences (`permission_prompt`, `idle_prompt`, `session_error`, etc.).
**Fix options**:
- A) Change all `notify()` category values to match the `eventTypes` keys
- B) Change the `eventTypes` keys to match the categories used in `notify()` calls
- C) Add a mapping function in `notify()` that normalizes categories to eventType keys
Option A is the cleanest since the eventTypes keys match the hook event names from Claude Code.
### Issue 2: `session_error` hardcoded in save
In `saveAppSettings()` at line 9425-9429, `session_error` has its browser setting hardcoded to mirror `permission_prompt`'s browser toggle and its audio is always `false`. There is no dedicated UI row for session errors. Similarly, `token_milestone` is hardcoded to `enabled: true, browser: false, audio: false` with no UI controls (lines 9435-9439).
### Issue 3: Subagent spawn/complete share a single UI row
Both `subagent_spawn` and `subagent_complete` are controlled by a single "Subagent activity" row in the UI (lines 9445-9454). This is intentional but worth noting.
### Issue 4: Mobile default discoverability
Mobile users have notifications disabled by default. There is no onboarding prompt or toast suggesting they enable notifications. A user on mobile would need to find Settings > Notifications and enable the master toggle.
### Issue 5: Server-side prefs are write-only in practice
Because localStorage always wins over server prefs (unless localStorage is empty), the server copy of notification preferences is effectively a one-time bootstrap for new devices. Changes made on one device do not propagate to another device that already has local prefs.