Files
Developer caadd59441 chore: archive 15 completed OpenSpec changes
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.
2026-06-12 14:26:55 +00:00

12 KiB
Raw Permalink Blame History

Below is the complete explore.md content for the Notification Center SDD phase. Please write it to:

/home/alex/projects/headquarter/openspec/changes/notification-center/explore.md


# SDD Explore — Notification Center

## 1. Current State of Notification Infrastructure

### Backend
- **InstanceEventBus** (`apps/api/src/services/event_bus.py`) — Singleton in-memory pub/sub bus with typed callbacks. Supports per-event-type subscriptions and a wildcard `"*"` catch-all. Exceptions are isolated so one failing subscriber does not break others. Currently single-process only.
- **HealthMonitor** (`apps/api/src/services/health_monitor.py`) — Background polling task that checks container/tunnel health and publishes `instance.health_changed` and `instance.error` events via the bus.
- **Lifecycle Hooks** (`apps/api/src/services/lifecycle_hooks.py`) — `publish_lifecycle_event()` builds a standard payload, writes an audit row to the `instance_events` table, and publishes to the bus. Used extensively by the tool-instances API (`instance.created`, `instance.started`, `instance.stopped`, etc.).
- **SSE Stream** (`apps/api/src/api/events.py`) — `GET /events/stream` subscribes to the wildcard `"*"` topic and pushes JSON payloads to **all** authenticated users. There is no per-user filtering. It enforces a 5-connection limit per user and drops oldest events when the queue is full.
- **Audit Model** (`apps/api/src/models/instance_event.py`) — `InstanceEvent` persists event metadata, type, status, message, and `created_by` user ID. It is tied to `tool_instances.id` but is **not** a user-facing notification store.
- **User / Preferences** (`apps/api/src/models/user.py`, `apps/api/src/models/user_config.py`) — `User` has a 1-to-1 `UserConfig` JSON blob (`config` column) used for theme, editor, git identity, etc. No notification-related keys exist yet.

### Frontend
- **AppShell** (`apps/web/src/components/app-shell.tsx`) — Global layout with a top `shell-header`. The right side (`header-actions`) currently holds a user chip and a logout button. This is the natural mount point for a bell icon + notification center dropdown.
- **Toast System** (`apps/web/src/state/toast.tsx`) — Global ephemeral toast context. Supports `info`, `success`, `warning`, `error` with configurable duration. Toasts are stored in React state and auto-dismiss.
- **Event Bridge** (`apps/web/src/components/event-toast-bridge.tsx`, `apps/web/src/components/toast-rules.ts`) — Listens to the `EventContext`, deduplicates instance events (1-second window), and maps them to toasts (e.g., `instance.error` → red toast).
- **EventProvider / useEvents** (`apps/web/src/state/events.tsx`, `apps/web/src/hooks/use-events.ts`) — Manages a single global SSE connection with exponential-backoff reconnect and 401/429 handling. Events are accumulated in a plain array in state.
- **Icons** (`apps/web/src/utils/icons.ts`) — Uses `@phosphor-icons/react`. No `bell` icon is currently registered.
- **Styling** (`apps/web/src/styles.css`) — Header uses flex layout with `backdrop-filter: blur`. Existing badge styles (`nav-badge`, `mobile-nav-badge`) can be reused or extended for an unread count.

## 2. Gaps Between Toast-Only and a Full Notification Center

| Gap | Impact |
|-----|--------|
| **No persistent notification store** | Missed events are lost forever if the user is offline or the toast expires. |
| **No per-user event filtering** | SSE broadcasts all instance events to every user. Users may receive irrelevant toasts. |
| **No read/unread/dismiss lifecycle** | Toasts are purely ephemeral; there is no concept of “mark as read” or “dismiss”. |
| **No historical API** | Users cannot revisit past notifications. |
| **No categorization / severity model** | Events are raw strings (`instance.error`). No structured category (system, container, security, etc.). |
| **No user preferences** | Cannot mute specific notification types or choose toast vs. silent delivery. |
| **No UI surface for a list** | No dropdown, popover, or panel component exists for listing notifications. |
| **No mobile-specific notification UI** | Mobile header is absent (mobile uses bottom nav). Need to decide where the bell lives on small screens. |
| **No non-instance notification sources** | Only container/health events are wired. System messages, build failures, or billing alerts have no pipeline. |

## 3. Key Files and Integration Points

### Backend — New / Modified
| File | Role |
|------|------|
| `apps/api/src/models/notification.py` | New SQLAlchemy model: `Notification` (user-scoped, read/unread, dismissed, category, payload). |
| `alembic/versions/…_add_notifications.py` | Migration for the new table + indexes on `(user_id, read_at)` and `(user_id, created_at)`. |
| `apps/api/src/services/notification_service.py` | New service: subscribes to event-bus topics, fans out per-user `Notification` rows. |
| `apps/api/src/api/notifications.py` | New FastAPI router: `GET /notifications`, `PATCH /notifications/{id}/read`, `POST /notifications/mark-all-read`, `DELETE /notifications/{id}`. |
| `apps/api/src/main.py` | Register the new router and import the `Notification` model for Alembic discovery. |
| `apps/api/src/api/events.py` | Decide whether to multiplex notification events into SSE or keep REST polling only. |
| `apps/api/src/services/lifecycle_hooks.py` | Optionally shift from “publish raw event” to “publish raw event + call notification service”. |
| `apps/api/src/services/health_monitor.py` | Health state changes should feed into notification service. |
| `apps/api/src/models/user_config.py` | Extend JSON schema (or add new columns) for notification preferences (mute categories, disable toasts). |

### Frontend — New / Modified
| File | Role |
|------|------|
| `apps/web/src/components/notification-center.tsx` | Bell icon + dropdown panel with notification list, empty state, and actions (mark read, dismiss). |
| `apps/web/src/hooks/use-notifications.ts` | Fetch notifications, unread count, mark-read/dismiss mutations, optional optimistic updates. |
| `apps/web/src/state/notifications.tsx` | React context/provider for notification list and unread count. Could poll or be driven by SSE. |
| `apps/web/src/components/app-shell.tsx` | Mount `<NotificationCenter />` inside `header-actions`. Hide on mobile terminal view. |
| `apps/web/src/utils/icons.ts` | Add `"bell"` (Phosphor `Bell`) to `IconName` / `iconRegistry`. |
| `apps/web/src/styles.css` | Add dropdown/popover positioning, z-index layering, and notification-item hover states. |
| `apps/web/src/components/event-toast-bridge.tsx` | Coordinate with notification system to avoid duplicate toast + notification for the same event. |
| `apps/web/src/components/mobile-nav.tsx` | Consider adding a bell icon or a badge on the existing “Sessions” nav item on mobile. |

## 4. Risks and Unknowns

1. **SSE Scaling / Filtering**  
   The current SSE endpoint broadcasts every event to every connected user. Adding per-user notification filtering inside the same SSE loop will require either:
   - A separate SSE stream for notifications with user-scoped queues, or
   - Client-side filtering (simple but wastes bandwidth and leaks data).  
   **Recommendation:** Start with REST polling for the notification list (every 30 s + manual refresh) and keep the existing SSE for real-time instance events. A dedicated `notifications/stream` SSE can be a fast-follow.

2. **Single-Process Event Bus Limit**  
   `InstanceEventBus` is an in-memory singleton. If the API is ever scaled to multiple workers, events published in one process will not be visible in another. The notification service should be architected so that it can later be backed by a persistent message queue (e.g., Redis pub/sub) without changing its interface.

3. **User Identification for Instance Events**  
   Most instance events naturally map to `ToolInstance.owner_id`, but some actions (e.g., an admin stopping another users container) may need to notify a different user than the owner. The `publish_lifecycle_event` helper currently accepts `created_by`; the notification service should accept an explicit `target_user_id` parameter.

4. **Duplicate Surface (Toast vs. Center)**  
   Users will be annoyed if every notification produces both a toast and a center entry simultaneously. We need a preference layer (“Show toasts for: all / errors only / none”) and a mechanism for the toast bridge to check whether a notification was already ingested into the center.

5. **Mobile Real Estate**  
   The mobile layout does not have a top header. The notification center will need a home inside `MobileNav` (e.g., a bell icon that opens a bottom sheet) or inside the existing `ToolsBottomSheet`.

6. **Migration Safety**  
   Adding a high-write table (`notifications`) to the same database used for health checks and events could introduce write contention under heavy load. Indexes on `(user_id, created_at)` and a partial index on `read_at IS NULL` are essential from day one.

7. **No Existing Dropdown Component**  
   There is no reusable dropdown/popover in the design system. We will need to build one (or at least a positioned panel) and ensure it closes on outside click, handles focus, and works in both light and dark themes.

## 5. Recommended Architecture Approach

### Phase 1 — Core Backend (REST + DB)
1. **Model** — Create `Notification` table:
   - `id` (UUID PK)
   - `user_id` (FK → users.id, indexed)
   - `category` (str: `instance`, `system`, `health`, `security`)
   - `severity` (str: `info`, `warning`, `error`, `success`)
   - `title`, `message` (text)
   - `source_id`, `source_type` (nullable, e.g., `tool_instances.id`)
   - `metadata` (JSON)
   - `read_at` (datetime, nullable, indexed)
   - `dismissed_at` (datetime, nullable)
   - `created_at` (timestamp)
2. **Service**`NotificationService` with methods:
   - `create_notification(user_id, category, severity, title, message, …)`
   - `get_unread_count(user_id)`
   - `list_notifications(user_id, limit, offset, unread_only)`
   - `mark_read(notification_id)`, `mark_all_read(user_id)`, `dismiss(notification_id)`
3. **Bus Integration** — Subscribe `NotificationService` to relevant event types (or have `lifecycle_hooks` and `HealthMonitor` call it directly). Use `ToolInstance.owner_id` as the default `user_id`.
4. **API** — New FastAPI router under `/notifications` with the CRUD endpoints above.
5. **Preferences** — Extend `UserConfig` JSON with:
   - `notification_mute_categories: string[]`
   - `notification_toast_level: "all" | "errors" | "none"`

### Phase 2 — Frontend UI
1. **Icon** — Add `bell` to the Phosphor icon registry.
2. **Component**`<NotificationCenter />`:
   - Bell icon with an unread count badge.
   - Click opens a dropdown panel (positioned under the bell, right-aligned).
   - Panel contains a scrollable list of recent notifications, grouped by date.
   - Each row shows severity icon, title, relative timestamp, and a “Mark read” / “Dismiss” action.
   - Footer with “Mark all as read”.
3. **State**`NotificationProvider` + `useNotifications()` hook:
   - Poll `GET /notifications` every 30 seconds.
   - Poll `GET /notifications/unread` every 15 seconds for the badge.
   - Optimistically update local state on mark-read/dismiss.
4. **Integration** — Mount inside `AppShell` header-actions. Suppress bell on `isMobileTerminal`.
5. **Toast Coordination** — Update `EventToastBridge` to respect `notification_toast_level` before showing a toast. Consider adding a `notification_id` to the toast metadata so clicking the toast could open the notification center.

### Phase 3 — Real-Time (Fast Follow)
- Add a lightweight `notifications/stream` SSE endpoint that pushes only to the owning user.
- Replace polling in `NotificationProvider` with SSE for instantaneous badge updates.

### Modularity Guidelines
- **Sources are decoupled:** Any backend module can call `notification_service.create_notification(...)`. The event bus remains the transport for raw events; the notification service is the consumer that turns them into user-visible rows.
- **Category extensibility:** New sources (e.g., future billing or team-mention system) only need to supply `category`, `severity`, and `target_user_id`.
- **Frontend reusability:** The notification list item component should accept a generic `NotificationItem` interface so new categories can render custom icons or deep links without rewriting the list.

---

**Next Step:** Proceed to **SDD Specification** to lock down the exact API schema, component props, and database migration details.