caadd59441
Move the following audited-and-implemented changes into openspec/changes/archive/2026-06-12-completed-changes-archive/: - backend-frontend-refactoring - config-profile-git-mounts - config-profile-includes-ui - config-profile-multi-repo-mounts - container-monitoring-notifications - git-mount-url-validation - home-path-expansion - mobile-terminal-ux - mount-specificity-ordering - notification-center - persistent-terminal-sessions - session-list-overhaul - ssh-key-mounting - terminal-fullscreen-unified-header - tool-session-progress-and-updates Also regenerated .pi-map*.md files for openspec/changes so the remaining active changes (multi-session-terminal-ux, reorganize-long-files, working-copies, workspace-first-ui) reflect the new layout.
7.9 KiB
7.9 KiB
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"toIconNameunion inapps/web/src/utils/icons.ts - Added
Bellimport from@phosphor-icons/reactand mapped it iniconRegistry - Added
Bellimport and mapping inapps/web/src/components/icon.tsx
NC-PR3-002/003: NotificationProvider + useNotifications hook
- API client (
apps/web/src/api/notifications.ts): Typed wrappers forGET /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,dismisswith revert on failure
- Maintains
- 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
severityto existing Phosphor icons) - Shows
titleand relative timestamp viaformatRelativeTime - 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
- Displays severity icon (mapped from
- 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
NotificationItemcomponents - Empty state: "No notifications"
- Footer "Mark all as read" button
- Calls
refreshList()immediately on open - Hidden when
isMobileTerminalis true
- Bell icon button with
NC-PR3-006: AppShell integration
- Wrapped authenticated app layout with
<NotificationProvider>(insideEventProvider+ToastProvider) - Mounted
<NotificationCenter isMobileTerminal={isMobileTerminal} />insideheader-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
apps/web/src/api/notifications.ts(new) — API client for notification endpointsapps/web/src/utils/icons.ts— Added"bell"toIconNameandiconRegistryapps/web/src/components/icon.tsx— AddedBellimport and mappingapps/web/src/utils/time.ts(new) —formatRelativeTimeutilityapps/web/src/state/notifications.tsx(new) —NotificationProviderwith polling + optimistic mutationsapps/web/src/hooks/use-notifications.ts(new) — Consumer hookapps/web/src/hooks/use-notifications.test.tsx(new) — 9 hook testsapps/web/src/components/notification-item.tsx(new) — Single notification rowapps/web/src/components/notification-item.test.tsx(new) — 6 item testsapps/web/src/components/notification-center.tsx(new) — Bell + dropdown panelapps/web/src/components/notification-center.test.tsx(new) — 10 center testsapps/web/src/styles.css— Notification center + item CSS utilitiesapps/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
# 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
- Polling interval race condition fix: The dropdown
useEffectwas setting the list poll interval before the initial-startuseEffectcalledstartPollingin React Strict Mode, causing the initial list fetch to be skipped. Fixed by requiringunreadIntervalRef.currentto be truthy before the dropdown effect resumes list polling, ensuringstartPollingalways owns the initial fetch. - Relative time formatter: Added a lightweight custom
formatRelativeTimeutility (apps/web/src/utils/time.ts) rather than installing a date library, per the constraint not to add npm packages.
Surprises / Decisions
- React Strict Mode interval race: The order of effect execution in Strict Mode caused
listIntervalRefto be populated beforestartPollingchecked it, suppressing the initial list fetch. Adding&& unreadIntervalRef.currentto the dropdown resume branch fixed this. - No npm packages installed: All work used existing dependencies. Custom utility for relative time instead of
date-fns. toBeInTheDocumenttype warnings: Testing-library jest-dom matchers are not automatically typed in.test.tsxfiles 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.