19242b4152
- EventToastBridge checks notification_toast_level and notification_mute_categories - toast-rules.ts: event-to-category/severity mapping functions - Settings page: notification preferences section (toast level dropdown, mute checkboxes) - Settings API types extended with notification preference fields - 17 frontend tests (toast-rules + bridge) - Preference hierarchy: mute categories → toast level → show/hide Quality gates: vitest 17 passed, tsc clean, eslint clean
19 KiB
19 KiB
Apply Progress: Notification Center
TDD Cycle Evidence (PR-1)
| Cycle | Task | Test File | RED | GREEN | Evidence |
|---|---|---|---|---|---|
| 1 | NC-PR1-004 (basic CRUD) | tests/unit/test_notification_service.py |
13 tests written against missing service | All 13 pass | pytest tests/unit/test_notification_service.py → 13 passed |
| 2 | NC-PR1-005 (edge cases) | tests/unit/test_notification_service.py |
Already included in cycle 1 | Added wrong-owner, mute-categories, cross-user isolation | Same 13 tests pass |
| 3 | NC-PR1-007 (basic endpoints) | tests/integration/test_notifications_api.py |
10 tests written against missing router | All 10 pass | pytest tests/integration/test_notifications_api.py → 10 passed |
| 4 | NC-PR1-008 (API edge cases) | tests/integration/test_notifications_api.py |
Already included in cycle 3 | Pagination, 404 ownership, mute categories at API layer | Same 10 tests pass |
| 5 | NC-PR1-010 (REFACTOR) | All files | — | ruff clean, no regressions | ruff check passes on all new files; existing unit tests 223 passed (4 pre-existing failures unrelated) |
TDD Cycle Evidence (PR-2)
| Cycle | Task | Test File | RED | GREEN | Evidence |
|---|---|---|---|---|---|
| 1 | NC-PR2-004 (producer flow) | tests/integration/test_notifications_lifecycle.py |
4 tests written against unwired producers | Wired lifecycle_hooks.py and health_monitor.py |
4 passed |
| 2 | NC-PR2-004 (severity mapping) | tests/integration/test_notifications_lifecycle.py |
Added error + warning severity tests | Already green from implementation | 6 passed |
| 3 | NC-PR2-003 (UserConfig schema) | src/api/user_config.py |
Schema extended with new optional fields | PATCH/GET endpoints validate correctly | Verified manually |
| 4 | NC-PR2-005 (REFACTOR) | All files | — | ruff clean, no regressions across 41 related tests | All pass |
TDD Cycle Evidence (PR-3)
| Cycle | Task | Test File | RED | GREEN | Evidence |
|---|---|---|---|---|---|
| 1 | NC-PR3-009 (hook tests) | src/hooks/use-notifications.test.tsx |
9 tests written against stub provider/hook | Implemented NotificationProvider + useNotifications |
9 passed |
| 2 | NC-PR3-008 (component tests) | src/components/notification-center.test.tsx |
10 tests written against stub component | Implemented NotificationCenter + NotificationItem |
10 passed |
| 3 | NC-PR3-005 (NotificationItem tests) | src/components/notification-item.test.tsx |
6 tests written against stub component | Implemented NotificationItem |
6 passed |
| 4 | NC-PR3-012 (REFACTOR) | All files | — | tsc --noEmit clean, eslint clean |
Zero errors |
Completed Tasks
PR-1: Backend Core
- NC-PR1-001: Alembic migration for
notificationstable - NC-PR1-002: SQLAlchemy
Notificationmodel (apps/api/src/models/notification.py) - NC-PR1-003: Export
Notificationinmodels/__init__.py - NC-PR1-004: NotificationService unit tests — basic CRUD (RED)
- NC-PR1-005: Implement
NotificationServicesingleton (GREEN) - NC-PR1-006: Service edge-case and isolation tests (TRIANGULATE)
- NC-PR1-007: API integration tests — basic endpoints (RED)
- NC-PR1-008: Implement FastAPI router + Pydantic schemas (GREEN)
- NC-PR1-009: API edge-case and ownership tests (TRIANGULATE)
- NC-PR1-010: Register router in
main.py+ importNotificationfor Alembic - NC-PR1-011: Code quality pass — ruff, test regressions, smoke tests (REFACTOR)
PR-2: Backend Integration
- NC-PR2-001: Wire
lifecycle_hooks.pytoNotificationService - NC-PR2-002: Wire
health_monitor.pytoNotificationService - NC-PR2-003: Extend
UserConfigschema for notification preferences - NC-PR2-004: Event producer integration tests (RED)
- NC-PR2-005: Verify producer tests pass and clean up (GREEN / REFACTOR)
PR-3: Frontend Core
- NC-PR3-001: Add "bell" icon to icon registry (
apps/web/src/utils/icons.ts,apps/web/src/components/icon.tsx) - NC-PR3-002:
NotificationProvidercontext with polling (apps/web/src/state/notifications.tsx) - NC-PR3-003:
useNotifications()hook (apps/web/src/hooks/use-notifications.ts) - NC-PR3-004:
NotificationCentercomponent — bell + dropdown panel (apps/web/src/components/notification-center.tsx) - NC-PR3-005:
NotificationItemcomponent — single row (apps/web/src/components/notification-item.tsx) - NC-PR3-006: AppShell integration — mount
NotificationCenterin header-actions (apps/web/src/components/app-shell.tsx) - NC-PR3-007: CSS styles for notification center (
apps/web/src/styles.css) - NC-PR3-008: Component tests for
NotificationCenter(apps/web/src/components/notification-center.test.tsx) - NC-PR3-009: Hook tests for
useNotifications(apps/web/src/hooks/use-notifications.test.tsx)
Files Changed
PR-1 Files
apps/api/alembic/versions/2026_05_29_add_notifications_table.py(new) — Alembic migrationapps/api/src/models/notification.py(new) — SQLAlchemy modelapps/api/src/models/__init__.py— ExportNotificationapps/api/src/services/notification_service.py(new) —NotificationServicesingletonapps/api/src/api/notifications.py(new) — FastAPI router + Pydantic schemasapps/api/src/api/__init__.py— Exportnotifications_routerapps/api/src/main.py— Register router, importNotificationfor Alembicapps/api/tests/unit/test_notification_service.py(new) — 13 unit testsapps/api/tests/integration/test_notifications_api.py(new) — 10 integration testsapps/api/tests/integration/test_models.py— Updated expected tables list
PR-2 Files
apps/api/src/services/lifecycle_hooks.py— WiredNotificationServiceafter event bus publishapps/api/src/services/health_monitor.py— WiredNotificationServiceafter state change event publishapps/api/src/api/user_config.py— Addednotification_mute_categoriesandnotification_toast_levelto Pydantic schemasapps/api/tests/integration/test_notifications_lifecycle.py(new) — 6 integration tests for event-to-notification flow
PR-3 Files
apps/web/src/api/notifications.ts(new) — API client for notification endpointsapps/web/src/utils/icons.ts— Added"bell"toIconNameunion andiconRegistryapps/web/src/components/icon.tsx— AddedBellimport and mappingapps/web/src/utils/time.ts(new) —formatRelativeTimeutilityapps/web/src/state/notifications.tsx(new) —NotificationProviderwith polling, optimistic mutations, visibility pauseapps/web/src/hooks/use-notifications.ts(new) —useNotificationsconsumer hookapps/web/src/hooks/use-notifications.test.tsx(new) — 9 hook tests (RED → GREEN)apps/web/src/components/notification-item.tsx(new) — Presentational notification rowapps/web/src/components/notification-item.test.tsx(new) — 6 component tests (RED → GREEN)apps/web/src/components/notification-center.tsx(new) — Bell icon, badge, dropdown panelapps/web/src/components/notification-center.test.tsx(new) — 10 component tests (RED → GREEN)apps/web/src/styles.css— Added notification center + item + dropdown CSS utilitiesapps/web/src/components/app-shell.tsx— MountedNotificationProviderandNotificationCenterin header-actions
Test Commands & Exit Codes
PR-1
# Unit tests for NotificationService (13 tests)
cd apps/api && python -m pytest tests/unit/test_notification_service.py -v
# Exit: 0 — 13 passed
# Integration tests for notifications API (10 tests)
cd apps/api && python -m pytest tests/integration/test_notifications_api.py -v
# Exit: 0 — 10 passed
# Existing unit tests (no regressions in our code)
cd apps/api && python -m pytest tests/unit/ -v
# Exit: 1 — 223 passed, 4 failed (pre-existing failures in test_config.py and test_git_repository_clone_preflight.py)
PR-2
# New integration tests for event producers (6 tests)
cd apps/api && python -m pytest tests/integration/test_notifications_lifecycle.py -v
# Exit: 0 — 6 passed
# Combined relevant test suite (41 tests)
cd apps/api && python -m pytest \
tests/unit/test_notification_service.py \
tests/integration/test_notifications_api.py \
tests/integration/test_notifications_lifecycle.py \
tests/unit/test_health_monitor.py \
tests/integration/test_events.py \
-v
# Exit: 0 — 41 passed
# Ruff linting on all PR-2 modified files
cd apps/api && python -m ruff check \
src/services/lifecycle_hooks.py \
src/services/health_monitor.py \
src/api/user_config.py \
tests/integration/test_notifications_lifecycle.py
# Exit: 0 — All checks passed
PR-3
# 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
PR-1
- SQLAlchemy
metadatacolumn name conflict:Base.metadatais reserved by SQLAlchemy DeclarativeBase. Usednotification_metadataas the Python attribute name with DB column name"metadata". In the Pydantic response model, usedField(serialization_alias="metadata")so the JSON API still exposesmetadataas specified. created_attype in Pydantic: Useddatetimeinstead ofstrto leverage FastAPI's automatic ISO serialization.
PR-2
- None. All mappings and behaviors match the design spec (section 1.3) and task requirements exactly.
PR-3
- Polling interval management: The provider uses two
useEffecthooks plusstartPolling/stopPollinghelpers. A race condition between the dropdown effect and the initial start effect in React Strict Mode was discovered and fixed by requiringunreadIntervalRef.currentto be truthy before the dropdown effect resumes list polling. This ensuresstartPollingalways owns initial list fetch. formatRelativeTimeutility: Design did not specify a relative-time formatter. Added a lightweight custom utility (apps/web/src/utils/time.ts) rather than installing a date library, per the constraint not to add npm packages.
Surprises / Decisions
PR-1
- SQLite
func.now()resolution:test_list_notifications_orders_by_created_at_descfailed because multiple rapid INSERTs got identical timestamps. Fixed by explicitly settingcreated_atoffsets in the test after creation. - Pre-existing integration test failures: ~40 integration tests fail due to missing
asyncpgmodule and direct PostgreSQL connection attempts in their custom setup code. These are unrelated to our changes. - Pre-existing
test_models.pyoutdated: Thetest_expected_tables_are_registeredassertion had a hardcoded set missing many newer tables. Updated it to include all current tables (includingnotifications).
PR-2
- Health monitor
test_health_monitor_unhealthy_creates_warning_notificationrequiredpublic_url: The health monitor only checks tunnel health wheninstance.public_urlis truthy. Without setting it on the test fixture instance,_derive_statusreturned"running"instead of"unhealthy", which created an"info"notification. Fixed by settingtest_instance.public_urlin the test before calling_check_instance. - Patch target for failure test: The
test_notification_failure_does_not_block_event_pipelinepatchessrc.services.lifecycle_hooks.notification_service.create_notification. This only works becauselifecycle_hooks.pyimportsnotification_serviceat module level, making the attribute resolvable byunittest.mock.patch. - No schema migration needed for UserConfig: Preferences are stored in the existing JSON
configblob, consistent with the existing pattern (theme, editor, git identity). No Alembic migration required.
PR-3
- React Strict Mode interval race: In
NotificationProvider, the dropdownuseEffectwas setting the list poll interval before the initial-startuseEffectcalledstartPolling, which causedstartPollingto skip its initialfetchList()call. Fixed by adding&& unreadIntervalRef.currentto the dropdown effect's resume branch, so it only resumes an already-active polling session. toBeInTheDocumenttype issues in tests: Testing-library jest-dom matchers type definitions were not automatically picked up in.test.tsxfiles. The tests run and pass at runtime; the TypeScript LSP warnings are cosmetic and do not block compilation or execution.- No npm packages installed: All frontend work was done with existing dependencies (
@phosphor-icons/react,react, etc.). Relative time formatting was implemented with a 20-line custom utility rather than addingdate-fnsor similar.
TDD Cycle Evidence (PR-4)
| Cycle | Task | Test File | RED | GREEN | Evidence |
|---|---|---|---|---|---|
| 1 | NC-PR4-001 (toast-rules mapping) | src/components/toast-rules.test.ts |
8 tests written against missing functions | Added mapEventToCategory + mapEventToSeverity |
npx vitest run src/components/toast-rules.test.ts → 8 passed |
| 2 | NC-PR4-002 (bridge preference tests) | src/components/event-toast-bridge.test.tsx |
5 tests written against bridge without preference logic | Updated EventToastBridge with config fetch + preference checks |
npx vitest run src/components/event-toast-bridge.test.tsx → 5 passed |
| 3 | NC-PR4-004 (edge-case tests) | src/components/event-toast-bridge.test.tsx |
Added immediate preference change, mute override, dedup, unmapped event tests | Already green from implementation | npx vitest run src/components/event-toast-bridge.test.tsx → 9 passed |
| 4 | NC-PR4-005 (settings UI) | src/pages/settings.tsx |
— | Added notification controls + UserConfig type extension |
npx tsc --noEmit clean, npx eslint clean |
| 5 | NC-PR4-006 (REFACTOR) | All files | — | Full type check, lint, and regression test | 17 new tests pass; 25 existing tests pass; zero lint/type errors |
Completed Tasks
PR-4: Toast Coordination
- NC-PR4-001: Extend
toast-rules.tswithmapEventToCategoryandmapEventToSeverity - NC-PR4-002: Write
EventToastBridgepreference check tests (RED) - NC-PR4-003: Update
EventToastBridgewith preference checks (GREEN) - NC-PR4-004: Bridge edge-case and integration tests (TRIANGULATE)
- NC-PR4-005: Extend settings UI with notification preferences
- NC-PR4-006: Final quality pass — type check, lint, regression tests (REFACTOR)
Files Changed (PR-4)
apps/web/src/components/toast-rules.ts— AddedmapEventToCategoryandmapEventToSeverityapps/web/src/components/toast-rules.test.ts(new) — 8 unit tests for mapping functionsapps/web/src/components/event-toast-bridge.tsx— Fetches user config, listens foruserconfig:updated, checks preferences before showing toastsapps/web/src/components/event-toast-bridge.test.tsx(new) — 9 tests for preference-based suppression, immediate updates, dedup, unmapped eventsapps/web/src/api/settings.ts— Addednotification_toast_levelandnotification_mute_categoriestoUserConfig/UserConfigUpdateapps/web/src/pages/settings.tsx— Added notification preference controls (toast level select + mute category checkboxes), dispatchesuserconfig:updatedon save
Test Commands & Exit Codes (PR-4)
# Toast-rules mapping tests (8 tests)
cd apps/web && npx vitest run src/components/toast-rules.test.ts
# Exit: 0 — 8 passed
# EventToastBridge preference tests (9 tests)
cd apps/web && npx vitest run src/components/event-toast-bridge.test.tsx
# Exit: 0 — 9 passed
# All new PR-4 tests combined
cd apps/web && npx vitest run src/components/toast-rules.test.ts src/components/event-toast-bridge.test.tsx
# Exit: 0 — 17 passed
# Existing frontend tests (no regressions)
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 on modified files
cd apps/web && npx eslint src/components/toast-rules.ts src/components/toast-rules.test.ts src/components/event-toast-bridge.tsx src/components/event-toast-bridge.test.tsx src/api/settings.ts src/pages/settings.tsx --ext ts,tsx
# Exit: 0 — clean
Deviations from Design (PR-4)
- No global UserConfig context: The design assumed an existing user-config context. The frontend did not have one, so
EventToastBridgefetches config on mount viagetUserConfigand listens for auserconfig:updatedCustomEventdispatched by the settings page after a successful save. This achieves immediate preference updates without introducing a new provider.
Surprises / Decisions (PR-4)
- Bridge processes events before config loads: The initial
useEffectinEventToastBridgecould process events whileconfigis stillnull. Fixed by initializingconfigtonulland skipping the event-processing effect until config resolves. This prevents toasts from leaking before preferences are known. UserConfigtype extended without breaking existing consumers: Adding optional fields toUserConfigandUserConfigUpdateinapi/settings.tsdid not require changes tosessions.tsxordashboard.tsxbecause they only import the API functions, not the types.- Custom event for immediate updates: Using
window.dispatchEvent(new CustomEvent("userconfig:updated", { detail: updated }))insettings.tsxand listening inevent-toast-bridge.tsxis consistent with the existingrefresh-file-treecustom-event pattern used inrepo-workspace.tsx.
Remaining Tasks
- All PR-4 tasks complete.
PR Boundary
This progress covers PR-1, PR-2, PR-3, and PR-4. The Notification Center feature is fully implemented.