# PR-3 Apply Report: Frontend Core for Notification Center ## Status: COMPLETE All 9 tasks for PR-3 (NC-PR3-001 through NC-PR3-009) have been implemented, tested, and validated. ## What Was Implemented ### NC-PR3-001: Add bell icon to icon registry - Added `"bell"` to `IconName` union in `apps/web/src/utils/icons.ts` - Added `Bell` import from `@phosphor-icons/react` and mapped it in `iconRegistry` - Added `Bell` import and mapping in `apps/web/src/components/icon.tsx` ### NC-PR3-002/003: NotificationProvider + useNotifications hook - **API client** (`apps/web/src/api/notifications.ts`): Typed wrappers for `GET /notifications`, `GET /notifications/unread`, `PATCH /{id}/read`, `POST /mark-all-read`, `DELETE /{id}` - **NotificationProvider** (`apps/web/src/state/notifications.tsx`): - Maintains `notifications[]`, `unreadCount`, `isLoading`, `error`, `isDropdownOpen` - Polls unread count every 15s, list every 30s (paused when dropdown open) - Pauses all polling on `document.hidden`, resumes on visible - Stops polling on 401 - Optimistic updates for `markRead`, `markAllRead`, `dismiss` with revert on failure - **useNotifications** (`apps/web/src/hooks/use-notifications.ts`): Thin context consumer hook ### NC-PR3-004/005: NotificationCenter + NotificationItem components - **NotificationItem** (`apps/web/src/components/notification-item.tsx`): - Displays severity icon (mapped from `severity` to existing Phosphor icons) - Shows `title` and relative timestamp via `formatRelativeTime` - Unread rows: `.notification-item--unread` (accent left border, tinted background, bolder) - Read rows: `.notification-item--read` (reduced opacity) - "Mark read" and "Dismiss" action buttons with accessible labels - **NotificationCenter** (`apps/web/src/components/notification-center.tsx`): - Bell icon button with `aria-label="Notifications"` - Red badge with unread count, capped at "99+" - Dropdown panel with `role="dialog"`, opens on click, closes on outside-click or Escape - Scrollable list of `NotificationItem` components - Empty state: "No notifications" - Footer "Mark all as read" button - Calls `refreshList()` immediately on open - Hidden when `isMobileTerminal` is true ### NC-PR3-006: AppShell integration - Wrapped authenticated app layout with `` (inside `EventProvider` + `ToastProvider`) - Mounted `` inside `header-actions`, before user chip - Mobile terminal shell also wrapped with `NotificationProvider` ### NC-PR3-007: CSS styles - Added `.notification-center`, `.notification-bell`, `.notification-badge`, `.notification-dropdown` - Added `.notification-item`, `.notification-item--unread`, `.notification-item--read` - Added `.notification-empty`, `.notification-mark-all`, `.notification-dropdown-header/footer` - Responsive: dropdown width adjusts on mobile (`max-width: 360px`) - Light/dark theme compatible using existing CSS variables ### NC-PR3-008/009: Tests - **Hook tests** (`src/hooks/use-notifications.test.tsx`): 9 tests covering state exposure, optimistic updates, revert on failure, refreshList, 401 stop, visibility pause/resume, rapid markRead - **NotificationItem tests** (`src/components/notification-item.test.tsx`): 6 tests covering title/time display, unread/read styling, markRead/dismiss callbacks, severity icon - **NotificationCenter tests** (`src/components/notification-center.test.tsx`): 10 tests covering bell render, badge show/hide, dropdown open/close (click/outside/escape), empty state, item rendering, markAllRead call, refresh on open ## Changed Files 1. `apps/web/src/api/notifications.ts` *(new)* — API client for notification endpoints 2. `apps/web/src/utils/icons.ts` — Added `"bell"` to `IconName` and `iconRegistry` 3. `apps/web/src/components/icon.tsx` — Added `Bell` import and mapping 4. `apps/web/src/utils/time.ts` *(new)* — `formatRelativeTime` utility 5. `apps/web/src/state/notifications.tsx` *(new)* — `NotificationProvider` with polling + optimistic mutations 6. `apps/web/src/hooks/use-notifications.ts` *(new)* — Consumer hook 7. `apps/web/src/hooks/use-notifications.test.tsx` *(new)* — 9 hook tests 8. `apps/web/src/components/notification-item.tsx` *(new)* — Single notification row 9. `apps/web/src/components/notification-item.test.tsx` *(new)* — 6 item tests 10. `apps/web/src/components/notification-center.tsx` *(new)* — Bell + dropdown panel 11. `apps/web/src/components/notification-center.test.tsx` *(new)* — 10 center tests 12. `apps/web/src/styles.css` — Notification center + item CSS utilities 13. `apps/web/src/components/app-shell.tsx` — Provider + component integration ## TDD Cycle Evidence | Cycle | Task | RED | GREEN | Evidence | |-------|------|-----|-------|----------| | 1 | Hook tests | 9 tests written against stub provider/hook | Implemented `NotificationProvider` + `useNotifications` | `npx vitest run src/hooks/use-notifications.test.tsx` → 9 passed | | 2 | NotificationItem tests | 6 tests written against stub component | Implemented `NotificationItem` | `npx vitest run src/components/notification-item.test.tsx` → 6 passed | | 3 | NotificationCenter tests | 10 tests written against stub component | Implemented `NotificationCenter` | `npx vitest run src/components/notification-center.test.tsx` → 10 passed | | 4 | REFACTOR | — | Type check + lint clean | `npx tsc --noEmit` → 0; `npx eslint ...` → 0 | ## Test Commands & Exit Codes ```bash # Hook tests (9 tests) cd apps/web && npx vitest run src/hooks/use-notifications.test.tsx # Exit: 0 — 9 passed # NotificationItem tests (6 tests) cd apps/web && npx vitest run src/components/notification-item.test.tsx # Exit: 0 — 6 passed # NotificationCenter tests (10 tests) cd apps/web && npx vitest run src/components/notification-center.test.tsx # Exit: 0 — 10 passed # All new frontend tests combined (25 tests) cd apps/web && npx vitest run src/hooks/use-notifications.test.tsx src/components/notification-item.test.tsx src/components/notification-center.test.tsx # Exit: 0 — 25 passed # Type check cd apps/web && npx tsc --noEmit # Exit: 0 — clean # Lint new/modified files cd apps/web && npx eslint src/api/notifications.ts src/state/notifications.tsx src/hooks/use-notifications.ts src/components/notification-item.tsx src/components/notification-center.tsx src/components/app-shell.tsx src/utils/icons.ts src/components/icon.tsx src/utils/time.ts src/hooks/use-notifications.test.tsx src/components/notification-item.test.tsx src/components/notification-center.test.tsx --ext ts,tsx # Exit: 0 — clean ``` ## Deviations from Design 1. **Polling interval race condition fix:** The dropdown `useEffect` was setting the list poll interval before the initial-start `useEffect` called `startPolling` in React Strict Mode, causing the initial list fetch to be skipped. Fixed by requiring `unreadIntervalRef.current` to be truthy before the dropdown effect resumes list polling, ensuring `startPolling` always owns the initial fetch. 2. **Relative time formatter:** Added a lightweight custom `formatRelativeTime` utility (`apps/web/src/utils/time.ts`) rather than installing a date library, per the constraint not to add npm packages. ## Surprises / Decisions 1. **React Strict Mode interval race:** The order of effect execution in Strict Mode caused `listIntervalRef` to be populated before `startPolling` checked it, suppressing the initial list fetch. Adding `&& unreadIntervalRef.current` to the dropdown resume branch fixed this. 2. **No npm packages installed:** All work used existing dependencies. Custom utility for relative time instead of `date-fns`. 3. **`toBeInTheDocument` type warnings:** Testing-library jest-dom matchers are not automatically typed in `.test.tsx` files in this project setup. Tests pass at runtime; TypeScript warnings are cosmetic. ## PR Boundary This PR covers PR-3 only (NC-PR3-001 through NC-PR3-009). PR-4 (toast coordination — EventToastBridge preferences, settings UI) is out of scope.