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.5 KiB
PR-1 Apply Report: Backend Core for Notification Center
Status: COMPLETE
All 11 tasks for PR-1 (NC-PR1-001 through NC-PR1-011) have been implemented, tested, and validated.
What Was Implemented
Database Layer
-
Alembic migration (
alembic/versions/2026_05_29_add_notifications_table.py)- Creates
notificationstable with all design-spec columns - FK
user_id->users.idwithON DELETE CASCADE - Index
idx_notifications_user_created_aton(user_id, created_at DESC) - Partial index
idx_notifications_user_unreadon(user_id, read_at)whereread_at IS NULL
- Creates
-
SQLAlchemy model (
src/models/notification.py)Notificationclass withUUIDPrimaryKeyMixin+Basenotification_metadataattribute mapped to DB column"metadata"(avoids SQLAlchemyBase.metadataconflict)- Exported from
src/models/__init__.py
Service Layer
- NotificationService singleton (
src/services/notification_service.py)create_notification(session, user_id, ...)— inserts row, returnsNotificationlist_notifications(session, user_id, ...)— returns(items, total)tuple, excludes dismissed, supportsunread_onlyandmute_categoriesget_unread_count(session, user_id)— counts unread + non-dismissedmark_read(session, notification_id, user_id)— setsread_at = now()mark_all_read(session, user_id)— bulk update, returns countdismiss(session, notification_id, user_id)— soft-delete viadismissed_at = now()- All methods enforce
user_idfiltering; wrong-owner raisesValueError("Notification not found")
API Layer
- FastAPI router (
src/api/notifications.py) mounted at/notificationsGET /notifications— paginated list withlimit,offset,unread_onlyquery params;limitcapped at 100GET /notifications/unread— returns{count: int}PATCH /notifications/{id}/read— marks single notification readPOST /notifications/mark-all-read— returns{marked_count: int}DELETE /notifications/{id}— soft-delete (dismiss), returns204- Reads
notification_mute_categoriesfromUserConfig.configJSON blob and passes tolist_notifications - Returns
404for non-owned or missing notifications - Pydantic
NotificationItemserializesnotification_metadataas"metadata"viaField(serialization_alias="metadata")
Registration
- Router imported and included in
src/main.py Notificationmodel imported insrc/main.pywith# noqa: F401for Alembic autogenerate discoverynotifications_routerexported fromsrc/api/__init__.py
Tests
- 13 unit tests (
tests/unit/test_notification_service.py) covering:- Create, list, unread count, mark read, mark all read, dismiss
- Cross-user isolation, wrong-owner 404-equivalent, mute categories filtering
- Dismissed excluded from unread count, mark-all-read affects only caller
- 10 integration tests (
tests/integration/test_notifications_api.py) covering:- Auth requirements, ownership isolation, pagination
- Mark read / dismiss endpoints and 404 for other users
- Mute categories filter at API layer
Changed Files
apps/api/alembic/versions/2026_05_29_add_notifications_table.py(new)apps/api/src/models/notification.py(new)apps/api/src/models/__init__.pyapps/api/src/services/notification_service.py(new)apps/api/src/api/notifications.py(new)apps/api/src/api/__init__.pyapps/api/src/main.pyapps/api/tests/unit/test_notification_service.py(new)apps/api/tests/integration/test_notifications_api.py(new)apps/api/tests/integration/test_models.py
Test Evidence
RED -> GREEN -> TRIANGULATE Cycles
| Cycle | Task | RED | GREEN | Result |
|---|---|---|---|---|
| 1 | Service unit tests (basic CRUD) | 13 tests written against missing service | Implemented NotificationService |
13 passed |
| 2 | Service edge cases | Wrong-owner, mute categories, cross-user tests added | Already green from implementation | 13 passed |
| 3 | API integration tests (basic endpoints) | 10 tests written against missing router | Implemented router + schemas | 10 passed |
| 4 | API edge cases | Pagination, 404 ownership, mute categories at API layer | Already green from implementation | 10 passed |
| 5 | REFACTOR | — | Ruff clean, no regressions | All new files pass ruff |
Commands Run
# NotificationService unit tests (13 tests)
cd apps/api && python -m pytest tests/unit/test_notification_service.py -v
# Exit: 0 — 13 passed
# Notifications API integration tests (10 tests)
cd apps/api && python -m pytest tests/integration/test_notifications_api.py -v
# Exit: 0 — 10 passed
# Combined new tests
cd apps/api && python -m pytest tests/unit/test_notification_service.py tests/integration/test_notifications_api.py -v
# Exit: 0 — 23 passed
# Existing unit suite (no regressions from our changes)
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)
# Ruff linting on all new/modified files
cd apps/api && python -m ruff check \
src/models/notification.py src/models/__init__.py \
src/services/notification_service.py \
src/api/notifications.py src/api/__init__.py src/main.py \
alembic/versions/2026_05_29_add_notifications_table.py \
tests/unit/test_notification_service.py \
tests/integration/test_notifications_api.py \
tests/integration/test_models.py
# Exit: 0 — All checks passed
# Smoke tests
# GET /health -> 200
# GET /notifications (unauthenticated) -> 401
Deviations from Design
-
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 exposes"metadata"as specified in the design. -
Datetime types in Pydantic schemas: Used
datetimeinstead ofstrforread_at,dismissed_at, andcreated_atto leverage FastAPI's automatic ISO-8601 serialization.
Surprises / Decisions
-
SQLite
func.now()timestamp resolution:test_list_notifications_orders_by_created_at_descinitially failed because multiple rapid INSERTs received identical timestamps. Fixed by explicitly settingcreated_atoffsets in the test after creation. -
Pre-existing integration test failures: Approximately 40 integration tests fail due to missing
asyncpgmodule and direct PostgreSQL connection attempts in their custom setup code. These failures are unrelated to our changes. -
Pre-existing
test_models.pyoutdated: Thetest_expected_tables_are_registeredassertion had a hardcoded set missing many newer tables (including our newnotificationstable). Updated it to include all current tables.
PR Boundary
This PR covers PR-1 only (NC-PR1-001 through NC-PR1-011). PR-2 (backend integration — wiring lifecycle_hooks.py and health_monitor.py) and PR-3/PR-4 (frontend) are out of scope and await this PR.
Risks
- Low: The migration uses
sa.JSON()which is compatible with both PostgreSQL and SQLite. The partial index usespostgresql_wherewhich is PostgreSQL-specific but safely ignored by SQLite. - Low:
notification_metadata->"metadata"serialization alias is a new pattern in the codebase but is explicitly tested via integration tests. - None: No changes to existing production code paths; all changes are additive.