4-layer notification system in `NotificationManager` class:
1.**In-app drawer** - Sliding panel with badge on bell icon, grouping within 5s windows
2.**Tab title flash** - `setInterval` at 1500ms, warning emoji + unread count when tab hidden
3.**Browser Notification API** - OS-level notifications, rate-limited 1 per 3s, auto-close at 8s
4.**Audio alerts** - Web Audio API 660Hz sine wave beep, 150ms duration
Separate from NotificationManager: **CSS tab alert system** driven by `pendingHooks` state machine (red blink for action, yellow for idle).
## Bugs Found & Fixed
### CRITICAL
| # | Bug | Fix | Files |
|---|-----|-----|-------|
| 1 | **Category/EventType key mismatch** - Per-event notification settings (On/Browser/Sound checkboxes) were completely non-functional. `notify()` used categories like `hook-permission` but `eventTypes` keys were `permission_prompt`. Lookup always failed, falling through to legacy urgency-based logic. | Added `categoryToEventType` mapping object in `notify()` method | `app.js:966-984` |
| 2 | **Cache invalidation bug** - `broadcast()` checked `event === 'respawn:'` (exact match) but all respawn events are `respawn:stateChanged` etc. Respawn state changes never invalidated cached state. | Changed to `event.startsWith('respawn:')` | `server.ts:4648` |
### HIGH
| # | Bug | Fix | Files |
|---|-----|-----|-------|
| 3 | **`session_error.browser` setting** used wrong checkbox - saved from `eventPermissionBrowser` instead of its own value | Changed to preserve current pref value with fallback | `app.js:9516` |
| 4 | **Dead hook events** - `hook:teammate_idle` and `hook:task_completed` broadcast by backend but no frontend handlers | Added SSE listeners with appropriate notifications | `app.js:2924-2950` |
| 5 | **`respawn:error` silently dropped** - No frontend handler for respawn errors | Added SSE listener with critical notification | `app.js:2630-2642` |
| 6 | **Subagent notifications decorative** - Settings had toggles but no code dispatched notifications | Wired `notify()` calls into subagent:discovered and subagent:completed handlers | `app.js:2989,3107` |
### MEDIUM
| # | Bug | Fix | Files |
|---|-----|-----|-------|
| 7 | **AudioContext autoplay policy** - No `resume()` call, first audio silently fails on browsers with autoplay restrictions | Added `audioCtx.state === 'suspended'` check with `resume()` | `app.js:1191` |
- **TeamWatcher is unintegrated** - The class exists in `team-watcher.ts` but is never imported in `server.ts`. Team events (member join/leave, task updates, inbox messages) are not broadcast via SSE. This is a larger feature gap, not a notification bug.
- **Browser notification rate limit is global** - A rapid succession of different event types only shows the first browser notification within 3s. This is by design for spam prevention.
- **No dynamic favicon** - Tab favicon is static; could be enhanced to show red/orange badge for unread notifications.
- **Per-session notification settings** - All notification prefs are global, no per-session customization.