- Add notifications table with Alembic migration
- Notification model with user-scoped indexing and partial index on unread
- NotificationService singleton with create/list/count/mark-read/dismiss
- FastAPI router: GET /notifications, GET /unread, PATCH /{id}/read,
POST /mark-all-read, DELETE /{id}
- Mute categories filtering from UserConfig
- 13 unit tests for NotificationService
- 10 integration tests for API endpoints
- Updated test_models.py with new table registration
Quality gates: pytest 23 new passed, ruff clean
37 KiB
SDD Tasks: Notification Center
Review Workload Forecast
| Field | Value |
|---|---|
| Estimated changed lines | ~1,800 total (PR-1 ~600; PR-2 ~250; PR-3 ~700; PR-4 ~250) |
| 400-line budget risk | High |
| Chained PRs recommended | Yes |
| Suggested split | PR 1 (Backend Core) → PR 2 (Backend Integration) → PR 3 (Frontend Core) → PR 4 (Toast Coordination) |
| Delivery strategy | auto-chain |
| Chain strategy | stacked-to-main |
Decision needed before apply: No
Chained PRs recommended: Yes
Chain strategy: stacked-to-main
400-line budget risk: High
Note: PR-1 (~600 lines) and PR-3 (~700 lines) exceed the 400-line review budget. PR-3 in particular carries High risk. Tasks within each PR are grouped into autonomous work units. If review fanout is available, PR-3 can be split into (a) Provider + Hook + Styles and (b) NotificationCenter + NotificationItem + AppShell integration. PR-1 can be split into (a) Migration + Model + Service and (b) Router + Registration + Tests.
PR-1: Backend Core
Goal: Establish the persistent notification backend: database schema, SQLAlchemy model, NotificationService singleton, FastAPI router with Pydantic schemas, and comprehensive unit + integration tests.
Estimated Lines: ~600 Review Risk: Medium
NC-PR1-001: Create Alembic migration for notifications table
Description:
Write an Alembic revision that creates the notifications table with all columns, constraints, indexes, and the foreign key to users.id as specified in the design.
Files to modify:
apps/api/alembic/versions/2026_05_29_add_notifications_table.py(new)
Acceptance criteria:
- Migration creates
notificationstable with columns:id,user_id,category,severity,title,message,source_type,source_id,metadata,read_at,dismissed_at,created_at. - Foreign key
user_idreferencesusers.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. upgrade()anddowngrade()are both implemented and passalembic upgrade head/alembic downgrade -1.- Migration depends on current
headrevision.
Estimated effort: Small (2–3 hours) Dependencies: None
NC-PR1-002: Create SQLAlchemy Notification model and export
Description:
Add the Notification SQLAlchemy model following the existing UUIDPrimaryKeyMixin + Base pattern. Export it from models/__init__.py for Alembic autogenerate discovery.
Files to modify:
apps/api/src/models/notification.py(new)apps/api/src/models/__init__.py
Acceptance criteria:
Notificationmodel matches the design schema exactly with correct types (UUID,String(32),String(16),String(255),Text,JSONB,DateTime(timezone=True)).user_idhasForeignKey("users.id", ondelete="CASCADE"),nullable=False,index=True.read_atandcreated_atare indexed.metadatacolumn defaults to{}.- Model is exported in
models/__init__.py. alembic revision --autogenerateproduces no drift against the hand-written migration.
Estimated effort: Small (2–3 hours) Dependencies: NC-PR1-001
NC-PR1-003: [RED] Write NotificationService unit tests — basic CRUD
Description:
Write failing pytest unit tests for NotificationService covering create, list, count, mark_read, mark_all_read, and dismiss happy paths.
Files to modify:
apps/api/tests/unit/test_notification_service.py(new)
Acceptance criteria:
test_create_notification: assert row inserted with correct values,read_atNULL,dismissed_atNULL.test_list_notifications_orders_by_created_at_desc: 3 rows inserted, newest first.test_list_notifications_excludes_dismissed: dismissed row not returned.test_list_notifications_unread_only:unread_only=Truereturns only unread.test_get_unread_count: 5 rows, 2 unread → count is 2.test_mark_read_sets_read_at:read_atis not NULL after call.test_mark_all_read_affects_all_unread: all unread rows updated.test_dismiss_sets_dismissed_at:dismissed_atis not NULL after call.- Tests use
db_sessionfixture and create testUserrows in session.
Estimated effort: Small (3–4 hours) Dependencies: NC-PR1-002
NC-PR1-004: [GREEN] Implement NotificationService
Description:
Implement the NotificationService singleton with all methods. The service accepts AsyncSession explicitly and filters all queries by user_id.
Files to modify:
apps/api/src/services/notification_service.py(new)
Acceptance criteria:
create_notification(session, user_id, *, category, severity, title, ...)inserts row and returnsNotification.list_notifications(session, user_id, *, limit=20, offset=0, unread_only=False, mute_categories=None)returns(items, total)tuple, excludesdismissed_at IS NOT NULL, orders bycreated_at DESC.get_unread_count(session, user_id)counts rows whereread_at IS NULLanddismissed_at IS NULL.mark_read(session, notification_id, user_id)setsread_at = now(), returns updated row; raises 404-equivalent if not found or not owned.mark_all_read(session, user_id)setsread_at = now()on all unread rows for user; returns count updated.dismiss(session, notification_id, user_id)setsdismissed_at = now(); raises 404-equivalent if not found or not owned.- All methods filter by
user_id. NC-PR1-003tests pass.
Estimated effort: Medium (4–5 hours) Dependencies: NC-PR1-003
NC-PR1-005: [TRIANGULATE] NotificationService edge-case and isolation tests
Description: Add unit tests for cross-user isolation, wrong-owner failures, mute category filtering, and partial index usage.
Files to modify:
apps/api/tests/unit/test_notification_service.py
Acceptance criteria:
test_mark_read_wrong_owner_raises: User A creates notification; User B callsmark_read→ exception raised.test_dismiss_wrong_owner_raises: User A creates notification; User B callsdismiss→ exception raised.test_list_notifications_mute_categories: passmute_categories=["instance"]; instance rows excluded, system rows returned.test_get_unread_count_excludes_dismissed: unread but dismissed row → count is 0.test_get_unread_count_query_uses_partial_index: query plan usesidx_notifications_user_unread(verified viaEXPLAINor SQLite equivalent).
Estimated effort: Small (2–3 hours) Dependencies: NC-PR1-004
NC-PR1-006: [RED] Write API integration tests — basic endpoints
Description: Write failing integration tests for the notifications API router covering list, unread count, mark read, mark all read, and dismiss.
Files to modify:
apps/api/tests/integration/test_notifications_api.py(new)
Acceptance criteria:
test_list_requires_auth:GET /notificationswithout auth →401.test_list_returns_only_own_notifications: create for user A; user B lists → not in response.test_unread_count_endpoint: create 3 unread;GET /notifications/unread→{count: 3}.test_mark_read_endpoint: create unread;PATCH /notifications/{id}/read→200,read_atset.test_mark_all_read_endpoint: create 4 unread;POST /notifications/mark-all-read→{marked_count: 4}.test_dismiss_endpoint: create notification;DELETE /notifications/{id}→204; subsequent list excludes it.- Uses
authenticated_clientanddb_sessionfixtures.
Estimated effort: Small (3–4 hours) Dependencies: NC-PR1-004
NC-PR1-007: [GREEN] Implement FastAPI notifications router and Pydantic schemas
Description:
Create the FastAPI APIRouter for /notifications with all endpoints and Pydantic response models. Read mute_categories from UserConfig and pass to list_notifications.
Files to modify:
apps/api/src/api/notifications.py(new)apps/api/src/api/__init__.py
Acceptance criteria:
GET /notificationswithlimit,offset,unread_onlyquery params; returnsNotificationListResponse.GET /notifications/unreadreturnsUnreadCountResponse.PATCH /notifications/{id}/readreturnsNotificationItem;404if not owned.POST /notifications/mark-all-readreturnsMarkAllReadResponse.DELETE /notifications/{id}returns204 No Content;404if not owned.- Router reads
notification_mute_categoriesfrom user'sUserConfig.configand passes tolist_notifications. limitcapped at 100.- All endpoints use
get_current_user_id/get_db_sessiondependencies. - Router exported from
api/__init__.py. NC-PR1-006tests pass.
Estimated effort: Medium (4–5 hours) Dependencies: NC-PR1-006
NC-PR1-008: [TRIANGULATE] API edge-case and ownership tests
Description: Add integration tests for pagination, ownership enforcement, and mute categories filtering at the API layer.
Files to modify:
apps/api/tests/integration/test_notifications_api.py
Acceptance criteria:
test_list_pagination: create 25 notifications;limit=10&offset=10→ items length 10, total 25.test_mark_read_404_for_other_user: create for user A; user B PATCH →404.test_dismiss_404_for_other_user: user B DELETE user A's notification →404.test_mute_categories_filter_in_list: set user configmute_categories=["instance"], create instance + system notifications;GET /notificationsreturns only system.test_mark_all_read_affects_only_caller: user A has 3 unread, user B has 2; A calls mark-all-read → A=0, B=2.
Estimated effort: Small (2–3 hours) Dependencies: NC-PR1-007
NC-PR1-009: Register router in main.py and import model for Alembic
Description:
Import and include the notifications router in the FastAPI app. Import the Notification model in main.py for Alembic autogenerate discovery.
Files to modify:
apps/api/src/main.py
Acceptance criteria:
notifications_routerimported and included withapp.include_router(...).Notificationmodel imported inmain.py(F401 noqa comment if unused).- App boots without import cycles.
GET /healthstill returns200.GET /notificationsreturns401when unauthenticated (smoke test).
Estimated effort: Small (1 hour) Dependencies: NC-PR1-007
NC-PR1-010: [REFACTOR] Backend code quality and type safety pass
Description:
Run ruff check ., mypy ., and pytest on the new code. Fix any lint errors, type annotations, or docstring gaps. Ensure no print statements or debug logs remain.
Files to modify:
- Any of the above files with lint/type issues.
Acceptance criteria:
ruff check .passes with zero errors on new files.mypy .passes with zero type errors on new files.pytest tests/unit/test_notification_service.py tests/integration/test_notifications_api.pypasses.- All public methods have docstrings.
- No
print()or leftoverlogger.debugfrom development.
Estimated effort: Small (1–2 hours) Dependencies: NC-PR1-008, NC-PR1-009
PR-2: Backend Integration
Goal: Wire lifecycle hooks and health monitor to create notifications, extend UserConfig for preferences, and validate the end-to-end event producer flow.
Estimated Lines: ~250 Review Risk: Low
NC-PR2-001: Wire lifecycle_hooks.py to call NotificationService
Description:
After publish_lifecycle_event() publishes the raw event to InstanceEventBus, call NotificationService.create_notification() with user_id=tool_instance.owner_id. Wrap in try/except so event pipeline is never blocked.
Files to modify:
apps/api/src/services/lifecycle_hooks.py
Acceptance criteria:
publish_lifecycle_eventcallsnotification_service.create_notification(...)after bus publish.user_idis set toinstance.owner_id.category="instance".severitymapped:infofor created/started/stopped/restarted/deleted;errorfor error.titlederived from event type (e.g., "Container started").source_type="tool_instances",source_id=instance.id.- Service call wrapped in
try/except; on failure, error is logged withcorrelation_idand execution continues. - Original event bus publish and audit row insert are unaffected by notification failure.
Estimated effort: Small (2–3 hours) Dependencies: NC-PR1-010
NC-PR2-002: Wire health_monitor.py to call NotificationService
Description:
After HealthMonitor detects a state change and publishes the event, call NotificationService.create_notification() with user_id=instance.owner_id. Wrap in try/except.
Files to modify:
apps/api/src/services/health_monitor.py
Acceptance criteria:
_handle_state_changecallsnotification_service.create_notification(...)after bus publish.user_idis set toinstance.owner_id.category="health"for health changes;"instance"for errors.severitymapped:errorfor crash,warningfor unhealthy,infofor recovery.source_type="tool_instances",source_id=instance.id.- Service call wrapped in
try/except; on failure, error is logged withcorrelation_idand loop continues. - Original event bus publish and health check insert are unaffected.
Estimated effort: Small (2–3 hours) Dependencies: NC-PR1-010
NC-PR2-003: Extend UserConfig schema for notification preferences
Description:
Add notification_mute_categories and notification_toast_level to the UserConfigResponse and UserConfigUpdate Pydantic models. Apply mute_categories filtering in list_notifications.
Files to modify:
apps/api/src/api/user_config.pyapps/api/src/services/notification_service.py
Acceptance criteria:
UserConfigResponseincludesnotification_mute_categories: list[str] | None = Noneandnotification_toast_level: str | None = None.UserConfigUpdateincludes the same optional fields.list_notificationsinNotificationServiceacceptsmute_categoriesand filters withNotification.category.not_in(mute_categories).GET /users/me/configreturns new keys when present in JSON blob.PATCH /users/me/configpersists new keys into the JSON blob.- Existing config keys are unaffected.
Estimated effort: Small (2–3 hours) Dependencies: NC-PR1-010
NC-PR2-004: [RED] Write event producer integration tests
Description: Write integration tests that exercise real lifecycle and health monitor endpoints and assert notification rows are created for the instance owner.
Files to modify:
apps/api/tests/integration/test_notification_producers.py(new)
Acceptance criteria:
test_lifecycle_event_creates_notification: triggerinstance.startedvia lifecycle hook; assert notification row exists withcategory="instance",severity="info",user_id=owner_id.test_health_monitor_error_creates_notification: simulate health monitor detecting crash; assert notification row withseverity="error".test_notification_failure_does_not_block_event_pipeline: mockcreate_notificationto raise; assert event is still published and no exception escapes.test_notification_ownership_matches_instance_owner: create instance for user A; trigger event; assert notificationuser_idis A's ID, not the calling user's.- Uses
authenticated_client,db_session, andtest_project_and_repofixtures.
Estimated effort: Medium (3–4 hours) Dependencies: NC-PR2-001, NC-PR2-002, NC-PR2-003
NC-PR2-005: [GREEN / REFACTOR] Verify producer tests pass and clean up
Description: Run the producer integration tests, fix any failures, and do a final lint/type check on all modified files.
Files to modify:
- Any files with issues found during test runs.
Acceptance criteria:
pytest tests/integration/test_notification_producers.pypasses.ruff check .passes on modified files.mypy .passes on modified files.- No regressions in existing
pytestsuite.
Estimated effort: Small (1–2 hours) Dependencies: NC-PR2-004
PR-3: Frontend Core
Goal: Build the frontend notification surface: icon registry, React context with polling, hook, notification list components, styles, and AppShell integration.
Estimated Lines: ~700 Review Risk: High
NC-PR3-001: Add bell icon to icon registry
Description:
Register the Phosphor Bell icon in the frontend icon registry under the name "bell".
Files to modify:
apps/web/src/utils/icons.ts
Acceptance criteria:
"bell"added toIconNameunion type.bell: Belladded toiconRegistrymap.Bellimported from@phosphor-icons/react.<Icon name="bell" />renders without error in a quick manual check.
Estimated effort: Small (30 minutes) Dependencies: None (can be prepared before PR-2 merges)
NC-PR3-002: [RED] Write useNotifications hook tests
Description:
Write failing tests for the useNotifications hook covering state exposure, optimistic updates, and revert behavior.
Files to modify:
apps/web/src/hooks/use-notifications.test.ts(new)
Acceptance criteria:
test_returns_notifications_and_unreadCount_from_context: mock provider value; assert hook returns same array and count.test_optimistically_updates_on_markRead: callmarkRead; assert localread_atset andunreadCountdecremented before API resolves.test_reverts_optimistic_update_on_markRead_failure: mock API rejection; assert state reverted.test_optimistically_updates_on_dismiss: calldismiss; assert item removed and count decremented.test_reverts_optimistic_update_on_dismiss_failure: mock API rejection; assert item restored.test_calls_refreshList_when_invoked: assertGET /notificationscalled.
Estimated effort: Small (2–3 hours) Dependencies: NC-PR3-001
NC-PR3-003: [GREEN] Implement NotificationProvider context with polling
Description:
Create the NotificationProvider React context that polls the backend endpoints, manages notification list and unread count, and handles tab visibility pause/resume.
Files to modify:
apps/web/src/state/notifications.tsx(new)
Acceptance criteria:
- Context maintains
notifications: NotificationItem[]andunreadCount: number. - Polls
GET /notifications/unreadevery 15 seconds. - Polls
GET /notificationsevery 30 seconds when dropdown is closed. - Pauses all polling when
document.hiddenis true; resumes on visible. - On dropdown open: immediately fetches list, pauses 30s list poll.
- On dropdown close: restarts 30s list poll.
- On logout: stops polling and clears state.
- Polling errors are silently logged; next cycle proceeds.
- On
401response: stops all polling.
Estimated effort: Medium (4–5 hours) Dependencies: NC-PR3-002
NC-PR3-004: [GREEN] Implement useNotifications hook
Description:
Create the useNotifications() consumer hook that exposes state and mutation callbacks with optimistic updates.
Files to modify:
apps/web/src/hooks/use-notifications.ts(new)
Acceptance criteria:
- Hook returns
notifications,unreadCount,isLoading,error,markRead,markAllRead,dismiss,refreshList. markRead(id): optimistically setsread_atand decrementsunreadCount; callsPATCH /notifications/{id}/read; reverts on failure.markAllRead(): optimistically setsread_aton all items andunreadCount=0; callsPOST /notifications/mark-all-read; reverts on failure.dismiss(id): optimistically removes item and decrementsunreadCountif unread; callsDELETE /notifications/{id}; reverts on failure.refreshList(): callsGET /notificationsand updates state.- Errors are surfaced as
errorstate but not thrown. NC-PR3-002tests pass.
Estimated effort: Medium (3–4 hours) Dependencies: NC-PR3-003
NC-PR3-005: [TRIANGULATE] Hook edge-case and error handling tests
Description: Add tests for 401 handling, polling pause, and multiple rapid mutations.
Files to modify:
apps/web/src/hooks/use-notifications.test.ts
Acceptance criteria:
test_stops_polling_on_401: simulate 401; assert polling intervals cleared.test_pauses_polling_when_document_hidden: simulatevisibilitychangeto hidden; assertclearIntervalcalled.test_resumes_polling_when_document_visible: simulate hidden then visible; assert intervals restarted and immediate fetches fired.test_multiple_markRead_calls_decrement_correctly: mark 3 items read rapidly; assertunreadCountdecrements by 3.
Estimated effort: Small (2–3 hours) Dependencies: NC-PR3-004
NC-PR3-006: [RED] Write NotificationItem component tests
Description:
Write failing render tests for the NotificationItem presentational component.
Files to modify:
apps/web/src/components/notification-item.test.tsx(new)
Acceptance criteria:
test_displays_title_and_relative_time: render with sample data; assert title and relative time visible.test_applies_unread_styling_when_read_at_is_null: assert unread CSS class present.test_applies_read_styling_when_read_at_is_set: assert read CSS class present.test_calls_onMarkRead_when_mark_read_clicked: simulate click; assert callback with correct id.test_calls_onDismiss_when_dismiss_clicked: simulate click; assert callback with correct id.test_displays_severity_icon: assert severity icon element present.
Estimated effort: Small (2–3 hours) Dependencies: NC-PR3-001
NC-PR3-007: [GREEN] Implement NotificationItem component
Description: Build the presentational row component for a single notification.
Files to modify:
apps/web/src/components/notification-item.tsx(new)
Acceptance criteria:
- Accepts
notification: NotificationItem,onMarkRead: (id: string) => void,onDismiss: (id: string) => void. - Displays severity icon mapped from
severityto Phosphor icon (Info,Warning,XCircle,CheckCircle). - Displays
titleand relative timestamp (e.g., "2m ago"). - Unread rows have
.notification-item--unreadclass (bolder text, accent border, background tint). - Read rows have
.notification-item--readclass (reduced opacity). - Renders "Mark read" and "Dismiss" action buttons.
NC-PR3-006tests pass.
Estimated effort: Small (3–4 hours) Dependencies: NC-PR3-006
NC-PR3-008: [RED] Write NotificationCenter component tests
Description:
Write failing render and interaction tests for the NotificationCenter component.
Files to modify:
apps/web/src/components/notification-center.test.tsx(new)
Acceptance criteria:
test_renders_bell_icon: assert bell icon visible.test_shows_badge_when_unread_count_gt_0: provider stateunreadCount=3; assert badge text is "3".test_hides_badge_when_unread_count_is_0: assert badge not in document.test_opens_dropdown_on_bell_click: simulate click; assert dropdown panel visible.test_closes_dropdown_on_outside_click: open dropdown; click outside; assert panel not visible.test_closes_dropdown_on_escape: open dropdown; fireEscapekey; assert panel not visible.test_renders_empty_state_when_no_notifications: assert empty state text visible.test_renders_notification_items: list has 2 items; assert 2NotificationItemcomponents rendered.test_calls_markAllRead_on_footer_button_click: simulate click; assert mock called.test_refreshes_list_immediately_on_open: open dropdown; assertrefreshListmock called.
Estimated effort: Small (3–4 hours) Dependencies: NC-PR3-007
NC-PR3-009: [GREEN] Implement NotificationCenter component
Description:
Build the NotificationCenter component: bell icon with badge, dropdown panel with list, empty state, footer actions, outside-click/Escape close, and mobile terminal hiding.
Files to modify:
apps/web/src/components/notification-center.tsx(new)
Acceptance criteria:
- Renders bell icon (
<Icon name="bell" />). - Shows unread count badge when
unreadCount > 0; caps display at "99+". - Badge uses existing
nav-badgeCSS class. - Dropdown opens on bell click, closes on outside click or
Escape. - Dropdown is a positioned panel below the bell, right-aligned.
- Contains scrollable list of
NotificationItemcomponents. - Shows empty state message when list is empty (e.g., "No notifications").
- Footer has "Mark all as read" button calling
markAllRead(). - Calls
refreshList()immediately when opening. - Hidden when
isMobileTerminalis true. - Uses
useNotifications()hook. NC-PR3-008tests pass.
Estimated effort: Medium (4–5 hours) Dependencies: NC-PR3-008
NC-PR3-010: Add notification CSS styles
Description:
Add utility classes for the notification dropdown, items, badge, and empty state to styles.css.
Files to modify:
apps/web/src/styles.css
Acceptance criteria:
.notification-dropdownhas absolute positioning,z-indexabove header,max-height, scroll, shadow, and matches light/dark theme variables..notification-itemhas padding, border-bottom, hover state..notification-item--unreadhas distinct styling (accent left border, slightly different background)..notification-item--readhas reduced opacity..notification-badgereuses or extends existingnav-badgestyles..notification-emptyhas centered text and muted color.- Styles work in both light and dark themes.
Estimated effort: Small (2–3 hours) Dependencies: NC-PR3-009
NC-PR3-011: Integrate NotificationCenter into AppShell
Description:
Mount <NotificationCenter /> inside the AppShell header-actions area. Wrap with NotificationProvider at the appropriate level.
Files to modify:
apps/web/src/components/app-shell.tsx
Acceptance criteria:
<NotificationProvider>wraps the authenticated app layout (inside or alongsideEventProvider).<NotificationCenter />rendered insideheader-actionsdiv, before the user chip.- Component is hidden when
isMobileTerminalis true. - No visual regressions in existing header layout.
- Existing tests for
AppShellstill pass (or are updated if needed).
Estimated effort: Small (1–2 hours) Dependencies: NC-PR3-009, NC-PR3-010
NC-PR3-012: [REFACTOR] Frontend code quality and type check pass
Description:
Run npm run typecheck, npm run lint, and frontend tests. Fix any errors. Verify accessibility (keyboard navigation, ARIA labels).
Files to modify:
- Any files with type/lint issues.
Acceptance criteria:
npm run typecheckpasses with zero errors.npm run lintpasses with zero errors.npm test(orvitest run) passes for all new test files.- Bell icon has
aria-label="Notifications". - Dropdown panel has
role="menu"orrole="dialog"and appropriatearia-*attributes. - Mark read / dismiss buttons have accessible labels.
- No
console.logleft from development.
Estimated effort: Small (1–2 hours) Dependencies: NC-PR3-011
PR-4: Toast Coordination
Goal: Update the toast bridge to respect notification preferences, extend settings UI for preference controls, and verify coordination end-to-end.
Estimated Lines: ~250 Review Risk: Low
NC-PR4-001: Extend toast-rules.ts with category/severity mapping
Description:
Add mapEventToCategory and mapEventToSeverity functions to toast-rules.ts so the bridge can evaluate events against user preferences.
Files to modify:
apps/web/src/components/toast-rules.ts
Acceptance criteria:
mapEventToCategory(event)returns"instance"forinstance.*events,"health"forhealth.*,"system"otherwise.mapEventToSeverity(event)returns"error"forinstance.error/health.error;"warning"for unhealthy health changes;"info"for created/started/stopped/restarted/deleted;"success"for recovery to running.- Functions are pure and exported.
- Existing toast mapping behavior is preserved (no regressions in current tests).
- New functions have unit tests.
Estimated effort: Small (2–3 hours) Dependencies: PR-3 merged (frontend types available)
NC-PR4-002: [RED] Write EventToastBridge preference check tests
Description:
Write failing tests for the updated EventToastBridge that verify preference-based toast suppression.
Files to modify:
apps/web/src/components/event-toast-bridge.test.tsx(new)
Acceptance criteria:
test_shows_toast_when_level_is_all_and_category_not_muted: assert toast shown.test_suppresses_toast_when_level_is_none: assert no toast.test_suppresses_info_toast_when_level_is_errors: event severityinfo; assert no toast.test_shows_error_toast_when_level_is_errors: event severityerror; assert toast shown.test_suppresses_toast_when_category_is_muted: config mute contains event category; assert no toast.- Tests mock
useEventContext, user config context, andtoast-rules.tsas needed.
Estimated effort: Small (2–3 hours) Dependencies: NC-PR4-001
NC-PR4-003: [GREEN] Update EventToastBridge with preference checks
Description:
Modify EventToastBridge to read notification_toast_level and notification_mute_categories from user config and skip toasts based on the preference hierarchy.
Files to modify:
apps/web/src/components/event-toast-bridge.tsx
Acceptance criteria:
- Bridge reads user config (from existing settings API / context).
- Evaluation order: mute categories first, then toast level.
- If
notification_toast_level === "none": no toasts shown. - If
notification_toast_level === "errors": only toasts forseverity === "error". - If
notification_toast_level === "all": toasts shown as before. - If event category is in
notification_mute_categories: toast suppressed. - Backend notification creation is unaffected; bridge only controls toast surfacing.
NC-PR4-002tests pass.
Estimated effort: Small (2–3 hours) Dependencies: NC-PR4-002
NC-PR4-004: [TRIANGULATE] Bridge edge-case and integration tests
Description: Add tests for preference changes taking effect immediately, mixed mute + level constraints, and no regressions in existing deduplication.
Files to modify:
apps/web/src/components/event-toast-bridge.test.tsx
Acceptance criteria:
test_preference_change_is_immediate: config changes from"all"to"none"; next event suppressed.test_muted_category_overrides_all_level: level"all"but category muted; toast suppressed.test_deduplication_still_works_with_preferences: two identical allowed events within 1s → one toast.test_unmapped_event_defaults_to_info: unknown event type → category"system", severity"info".
Estimated effort: Small (1–2 hours) Dependencies: NC-PR4-003
NC-PR4-005: Extend settings UI with notification preferences
Description: Add notification preference controls to the existing General settings tab: a multi-select/checkbox group for mute categories and a select for toast level.
Files to modify:
apps/web/src/pages/settings.tsxapps/web/src/api/settings.ts
Acceptance criteria:
UserConfiginterface inapi/settings.tsincludesnotification_mute_categories?: string[]andnotification_toast_level?: "all" | "errors" | "none".UserConfigUpdateinterface includes the same optional fields.- General settings tab has a "Notifications" section.
- Toast level select with options: "All", "Errors only", "None".
- Mute categories checkboxes for known categories:
instance,system,health,security. - Preferences save via existing
updateUserConfigAPI. - Saved preferences persist after page reload.
- Default values:
notification_toast_level="all",notification_mute_categories=[].
Estimated effort: Small (3–4 hours) Dependencies: NC-PR4-003
NC-PR4-006: [REFACTOR] Final quality pass and verification
Description: Run full frontend type check, lint, and test suite. Do a manual smoke test of the notification center + toast coordination.
Files to modify:
- Any files with issues found.
Acceptance criteria:
npm run typecheckpasses.npm run lintpasses.npm testpasses for all new and modified test files.- Manual smoke test: trigger an
instance.errorevent → notification appears in dropdown → toast appears (if level="all") → mark read → badge clears. - Manual smoke test: set toast level to "none" → trigger event → no toast appears, but notification still created.
- No regressions in existing settings page functionality.
Estimated effort: Small (1–2 hours) Dependencies: NC-PR4-004, NC-PR4-005
Dependency Graph (PR Level)
PR-1: Backend Core
│
├─► NC-PR1-001 ──► NC-PR1-002
│
├─► NC-PR1-003 ──► NC-PR1-004 ──► NC-PR1-005
│
├─► NC-PR1-006 ──► NC-PR1-007 ──► NC-PR1-008
│
├─► NC-PR1-009
│
└─► NC-PR1-010
PR-2: Backend Integration (depends on PR-1 merged)
│
├─► NC-PR2-001
│
├─► NC-PR2-002
│
├─► NC-PR2-003
│
├─► NC-PR2-004
│
└─► NC-PR2-005
PR-3: Frontend Core (depends on PR-1/PR-2 merged)
│
├─► NC-PR3-001
│
├─► NC-PR3-002 ──► NC-PR3-003 ──► NC-PR3-004 ──► NC-PR3-005
│
├─► NC-PR3-006 ──► NC-PR3-007
│
├─► NC-PR3-008 ──► NC-PR3-009 ──► NC-PR3-010 ──► NC-PR3-011
│
└─► NC-PR3-012
PR-4: Toast Coordination (depends on PR-3 merged)
│
├─► NC-PR4-001
│
├─► NC-PR4-002 ──► NC-PR4-003 ──► NC-PR4-004
│
├─► NC-PR4-005
│
└─► NC-PR4-006
Task Summary
| PR | Task ID | Description | TDD Phase | Effort |
|---|---|---|---|---|
| 1 | NC-PR1-001 | Alembic migration for notifications table | — | S |
| 1 | NC-PR1-002 | SQLAlchemy Notification model and export | — | S |
| 1 | NC-PR1-003 | Service unit tests — basic CRUD | RED | S |
| 1 | NC-PR1-004 | Implement NotificationService | GREEN | M |
| 1 | NC-PR1-005 | Service edge-case and isolation tests | TRIANGULATE | S |
| 1 | NC-PR1-006 | API integration tests — basic endpoints | RED | S |
| 1 | NC-PR1-007 | Implement FastAPI router and Pydantic schemas | GREEN | M |
| 1 | NC-PR1-008 | API edge-case and ownership tests | TRIANGULATE | S |
| 1 | NC-PR1-009 | Register router in main.py | — | S |
| 1 | NC-PR1-010 | Backend code quality and type safety pass | REFACTOR | S |
| 2 | NC-PR2-001 | Wire lifecycle_hooks.py to NotificationService | — | S |
| 2 | NC-PR2-002 | Wire health_monitor.py to NotificationService | — | S |
| 2 | NC-PR2-003 | Extend UserConfig schema for preferences | — | S |
| 2 | NC-PR2-004 | Event producer integration tests | RED | M |
| 2 | NC-PR2-005 | Verify producer tests and clean up | GREEN / REFACTOR | S |
| 3 | NC-PR3-001 | Add bell icon to icon registry | — | S |
| 3 | NC-PR3-002 | useNotifications hook tests | RED | S |
| 3 | NC-PR3-003 | Implement NotificationProvider context | GREEN | M |
| 3 | NC-PR3-004 | Implement useNotifications hook | GREEN | M |
| 3 | NC-PR3-005 | Hook edge-case and error handling tests | TRIANGULATE | S |
| 3 | NC-PR3-006 | NotificationItem component tests | RED | S |
| 3 | NC-PR3-007 | Implement NotificationItem component | GREEN | S |
| 3 | NC-PR3-008 | NotificationCenter component tests | RED | S |
| 3 | NC-PR3-009 | Implement NotificationCenter component | GREEN | M |
| 3 | NC-PR3-010 | Add notification CSS styles | — | S |
| 3 | NC-PR3-011 | Integrate NotificationCenter into AppShell | — | S |
| 3 | NC-PR3-012 | Frontend code quality and type check pass | REFACTOR | S |
| 4 | NC-PR4-001 | Extend toast-rules.ts with mapping | — | S |
| 4 | NC-PR4-002 | EventToastBridge preference check tests | RED | S |
| 4 | NC-PR4-003 | Update EventToastBridge with preference checks | GREEN | S |
| 4 | NC-PR4-004 | Bridge edge-case and integration tests | TRIANGULATE | S |
| 4 | NC-PR4-005 | Extend settings UI with notification preferences | — | S |
| 4 | NC-PR4-006 | Final quality pass and verification | REFACTOR | S |
Total tasks: 31
Total estimated effort: ~100 hours (backend ~40h, frontend ~45h, integration ~15h)