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

184 lines
11 KiB
Markdown

# PR-1 Apply Progress: Backend Core for Container Monitoring & Notifications
## TDD Cycle Evidence
### EventBus (MON-PR1-005 + MON-PR1-010)
| Cycle | Action | Evidence |
|-------|--------|----------|
| RED | Wrote `test_event_bus.py` with imports to non-existent module | `pytest` collection error: `ModuleNotFoundError: No module named 'src.services.event_bus'` |
| GREEN | Implemented `event_bus.py` with singleton, subscribe, publish, unsubscribe, exception isolation | `pytest tests/unit/test_event_bus.py -v` → 6 passed |
| REFACTOR | Replaced `asyncio.iscoroutinefunction` with `inspect.iscoroutinefunction`; moved `Awaitable`/`Callable` to `collections.abc` | Tests still pass, ruff clean |
| TRIANGULATE | Added wildcard (`"*"`) support in `publish()` for SSE endpoint | Verified by SSE endpoint test logic |
### HealthMonitor (MON-PR1-006 + MON-PR1-011)
| Cycle | Action | Evidence |
|-------|--------|----------|
| RED | Wrote `test_health_monitor.py` with imports to non-existent module | `pytest` collection error: `ModuleNotFoundError: No module named 'src.services.health_monitor'` |
| GREEN | Implemented `health_monitor.py` with poll loop, state comparison, DB writes, event publication | `pytest tests/unit/test_health_monitor.py -v` → 6 passed |
| REFACTOR | Added per-instance exception handling in `_check_instance`; removed redundant try/except in `_run_check_cycle` | Tests still pass |
### Models (MON-PR1-003 + MON-PR1-004)
| Cycle | Action | Evidence |
|-------|--------|----------|
| RED | Wrote `test_monitoring_models.py` with imports to non-existent models | `pytest` collection error: `ModuleNotFoundError` for models |
| GREEN | Implemented `instance_event.py` and `health_check.py`; exported in `models/__init__.py` | `pytest tests/unit/test_monitoring_models.py -v` → 3 passed |
## Completed Tasks
- [x] MON-PR1-001: Alembic migration `2026_05_28_add_monitoring_tables.py` (creates both `instance_events` and `health_checks` with all indexes)
- [x] MON-PR1-002: SQLAlchemy models `InstanceEvent` and `HealthCheck`
- [x] MON-PR1-003: `InstanceEvent` model (`apps/api/src/models/instance_event.py`)
- [x] MON-PR1-004: `HealthCheck` model (`apps/api/src/models/health_check.py`)
- [x] MON-PR1-005: `InstanceEventBus` service (`apps/api/src/services/event_bus.py`)
- [x] MON-PR1-006: `HealthMonitor` background task (`apps/api/src/services/health_monitor.py`)
- [x] MON-PR1-007: SSE endpoint `GET /events/stream` (`apps/api/src/api/events.py`)
- [x] MON-PR1-008: Lifecycle hooks in `tool_instances.py` + `lifecycle_hooks.py` service
- [x] MON-PR1-009: Structured JSON logging in `logging_config.py` + `CorrelationIdMiddleware`
- [x] MON-PR1-010: Unit tests for EventBus (`apps/api/tests/unit/test_event_bus.py`)
- [x] MON-PR1-011: Unit tests for HealthMonitor (`apps/api/tests/unit/test_health_monitor.py`)
## Files Changed
### New Files
- `apps/api/alembic/versions/2026_05_28_add_monitoring_tables.py`
- `apps/api/src/models/instance_event.py`
- `apps/api/src/models/health_check.py`
- `apps/api/src/services/event_bus.py`
- `apps/api/src/services/health_monitor.py`
- `apps/api/src/services/correlation.py`
- `apps/api/src/services/lifecycle_hooks.py`
- `apps/api/src/api/events.py`
- `apps/api/tests/unit/test_event_bus.py`
- `apps/api/tests/unit/test_health_monitor.py`
- `apps/api/tests/unit/test_monitoring_models.py`
### Modified Files
- `apps/api/src/models/__init__.py` — export new models
- `apps/api/src/api/__init__.py` — export events router
- `apps/api/src/api/tool_instances.py` — lifecycle hook instrumentation
- `apps/api/src/logging_config.py` — JSON formatter + CorrelationIdFilter
- `apps/api/src/main.py` — register events router, CorrelationIdMiddleware, HealthMonitor lifespan
## Test Evidence
```bash
# New unit tests — all pass
$ cd apps/api && python -m pytest tests/unit/test_event_bus.py tests/unit/test_health_monitor.py tests/unit/test_monitoring_models.py -v
15 passed, 4 warnings in 0.98s
# Full unit suite — no regressions (4 pre-existing failures unrelated to this change)
$ cd apps/api && python -m pytest tests/unit/ -v
172 passed, 4 failed, 2 warnings in 6.57s
# Ruff linting on new/modified files
$ cd apps/api && python -m ruff check <new files>
All checks passed!
```
## Deviation from Design
1. **Single migration vs. two migrations**: Prompt listed MON-PR1-001 and MON-PR1-002 as separate migrations, but design.md specifies a single revision. Implemented as one migration `2026_05_28_add_monitoring_tables.py` creating both tables.
2. **`metadata` column name**: SQLAlchemy `DeclarativeBase` reserves `metadata` as a class attribute. Used `event_metadata` as the Python attribute name with `"metadata"` as the DB column name via `mapped_column("metadata", ...)`. The event payload still uses `metadata` key.
3. **Delete audit row**: The FK `ON DELETE CASCADE` on `instance_events.instance_id` means the audit row for `instance.deleted` cannot survive deletion. The row is inserted before `session.delete(instance)` and is cascade-deleted on commit. The event bus publication still occurs.
4. **SQLite test compatibility**: Used `JSON` instead of `JSONB` in the SQLAlchemy model to maintain SQLite test compatibility. The migration uses `sa.JSON()` which maps appropriately.
## Remaining Tasks (for PR-3)
- Integration tests for SSE endpoint (MON-PR1-012)
- Integration tests for lifecycle hooks
- E2E tests
- Performance tuning and documentation
---
# PR-2 Apply Progress: Frontend UI for Container Monitoring & Notifications
## TDD Cycle Evidence
### useEvents Hook (MON-PR2-001 + MON-PR2-002 + MON-PR2-006)
| Cycle | Action | Evidence |
|-------|--------|----------|
| RED | Wrote `use-events.test.ts` with mocks for non-existent `api/events.ts` and `hooks/use-events.ts` | `vitest` collection error: `Cannot find module '../api/events'` |
| GREEN | Implemented `types/events.ts`, `api/events.ts`, and `hooks/use-events.ts` with SSE connect + reconnect backoff | `vitest run src/hooks/use-events.test.ts` → 7 passed |
| REFACTOR | Extracted `probeEventStreamStatus` into `api/events.ts`; added `isMountedRef` guard to prevent state updates after unmount | Tests still pass |
| TRIANGULATE | Added 401 redirect test and 429 penalty test using `probeEventStreamStatus` | Both pass |
### Toast Rules (MON-PR2-003 + MON-PR2-007)
| Cycle | Action | Evidence |
|-------|--------|----------|
| RED | Wrote `toast-rules.test.ts` mocking `../state/toast` | `vitest` collection error: `Cannot find module '../state/toast'` |
| GREEN | Implemented `state/toast.tsx` (custom toast system) and `components/toast-rules.ts` | `vitest run src/components/toast-rules.test.ts` → 7 passed |
| TRIANGULATE | Added deduplication tests (within 1s and after 1s) | Tests pass |
### EventProvider + Integration (MON-PR2-004 + MON-PR2-005 + MON-PR2-007)
| Cycle | Action | Evidence |
|-------|--------|----------|
| RED | Attempted to mount `<ToastProvider>` in `app-shell.tsx` before component existed | Build error: `Cannot find module '../state/toast'` |
| GREEN | Created `state/events.tsx`, `components/event-toast-bridge.tsx`, and integrated all providers into `app-shell.tsx` | `tsc --noEmit` clean; app renders in tests |
## Completed Tasks
- [x] MON-PR2-001: `useEvents()` SSE hook with reconnect backoff (`apps/web/src/hooks/use-events.ts`)
- [x] MON-PR2-002: `events.ts` API client — EventSource wrapper + probe helper (`apps/web/src/api/events.ts`)
- [x] MON-PR2-003: Custom `Toast` system with severity, auto-dismiss, manual dismiss (`apps/web/src/state/toast.tsx`)
- [x] MON-PR2-004: `ToastContainer` that manages toast queue + stacking + CSS animations
- [x] MON-PR2-005: `EventProvider` context — wraps app, provides shared event stream
- [x] MON-PR2-006: Real-time status badge updates — replaced 30s polling in `instance-list.tsx` with SSE-driven updates
- [x] MON-PR2-007: Integrated into `app-shell.tsx` — mounts `ToastProvider` + `EventProvider` + `EventToastBridge`
- [x] MON-PR2-008: Frontend tests for `useEvents` (7 tests) and toast rules (7 tests)
## Files Changed
### New Files
- `apps/web/src/types/events.ts`
- `apps/web/src/api/events.ts`
- `apps/web/src/hooks/use-events.ts`
- `apps/web/src/hooks/use-events.test.ts`
- `apps/web/src/components/toast-rules.ts`
- `apps/web/src/components/toast-rules.test.ts`
- `apps/web/src/state/toast.tsx`
- `apps/web/src/state/events.tsx`
- `apps/web/src/components/event-toast-bridge.tsx`
### Modified Files
- `apps/web/src/components/app-shell.tsx` — mount providers
- `apps/web/src/components/instance-list.tsx` — remove 30s polling, add SSE status updates, add 60s refresh
- `apps/web/src/components/session-card.tsx` — update badge color mapping
- `apps/web/src/styles.css` — add status-badge and toast animation styles
## Test Evidence
```bash
# New frontend unit tests — all pass
$ cd apps/web && npx vitest run src/hooks/use-events.test.ts src/components/toast-rules.test.ts
14 passed
# Regression check on related pages/components
$ cd apps/web && npx vitest run src/hooks/use-events.test.ts src/components/toast-rules.test.ts src/pages/dashboard.test.tsx src/components/terminal-session-tabs.test.tsx src/components/protected-route.test.tsx
25 passed
# TypeScript check
$ cd apps/web && npx tsc --noEmit
# Exit code: 0
# Lint on new/modified files
$ cd apps/web && npx eslint <new ts/tsx files> --ext ts,tsx --report-unused-disable-directives --max-warnings 0
# Exit code: 0
```
## Deviation from Design
1. **No sonner dependency**: The orchestrator explicitly instructed not to install `sonner` because `npm install` hangs in this environment. Implemented a custom ~170-line toast system instead using pure React + inline CSS. It is API-compatible with the expected `toast.info/success/warning/error(message, opts)` contract.
2. **EventSource 401/429 detection**: Native `EventSource.onerror` does not expose HTTP status codes. Added `probeEventStreamStatus()` in `api/events.ts` that performs a short `fetch()` with `AbortController` timeout to detect 401/429 before reconnecting.
3. **App.tsx vs app-shell.tsx**: This codebase has no `App.tsx`; `AppShell` in `app-shell.tsx` is the layout component that wraps all authenticated routes. Providers were mounted there instead.
4. **Tunnel health badge removed from instance-list**: The 30s polling loop was the sole source of tunnel health data. After removing it, the "tunnel error" badge is redundant because SSE status transitions to `unhealthy` are reflected in the status badge itself.
## Remaining Tasks (for PR-3)
- Integration tests for SSE endpoint (MON-PR1-012)
- Integration tests for lifecycle hooks (MON-PR3-001)
- E2E tests for container start → toast and crash detection (MON-PR3-002 / MON-PR3-003)
- Performance tuning — connection limits, queue bounds, jitter (MON-PR3-004)
- Documentation updates (MON-PR3-005)
- Final cleanup and regression validation (MON-PR3-006)