diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f65411..34c0e5c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,306 @@
# Changelog
+## [0.28.3] — 2026-03-13
+
+### Summary
+
+ICD audit close-out and straggler sweep. Rolls in the remaining v0.28.2
+items (websocket doc, auth/enums reconciliation) since the 0.28.3 branch
+was cut before the final v0.28.2 changeset landed.
+
+### Changed
+
+#### WebSocket ICD (`websocket.md`) — Full Rewrite
+- Event envelope field corrected: `"event"` not `"type"` (matches Go
+ struct json tag `json:"event"`).
+- Room subscribe/unsubscribe documented as **planned, not implemented**.
+ `JoinRoom`/`LeaveRoom` exist as stubs but no client wiring exists;
+ all delivery uses `Hub.SendToUser()`.
+- Added payload shape documentation for 11 event types:
+ `message.created` (user + assistant variants), `typing.start`,
+ `typing.stop`, `typing.user`, `user.presence`, `user.mentioned`,
+ `notification.new`, `notification.read`, `workflow.assigned`,
+ `workflow.claimed`, `workflow.advanced`, `workflow.completed`,
+ `role.fallback`, `tool.call.*`.
+- Routing table rewritten with accurate Direction, Delivery mechanism,
+ and labels matching `events.routeTable` in code.
+- Noted that `workflow.claimed`, `workflow.advanced`, `workflow.completed`
+ use room-scoped `Bus.Publish()` which does not reach WebSocket clients
+ (rooms not wired). Forward-looking plumbing only.
+
+#### Auth ICD (`auth.md`)
+- Login/Register response shape corrected: added `token_type` ("Bearer")
+ and `expires_in` (900), removed phantom fields (`avatar_url`,
+ `created_at`, `last_login`) that `generateTokens` does not emit.
+- Register documents the admin-approval path (201 with message when
+ `is_active` is false).
+
+#### Enums ICD (`enums.md`)
+- Task run statuses: added missing `queued` status (present in DB CHECK
+ constraint, was omitted from enum doc).
+
+#### Presence Status Reconciliation
+- DB CHECK constraint: `online`, `away`, `offline`.
+- Runtime behavior: hub emits only `online` and `offline`. `away` is
+ reserved for future idle detection.
+- Documented this gap in both `websocket.md` and `enums.md` rather than
+ adding dead code or removing the DB constraint.
+
+#### Housekeeping
+- `VERSION` bumped to 0.28.3.
+- `CHANGELOG.md` updated with entries for 0.28.0 through 0.28.3.
+- `ROADMAP.md` updated: v0.28.2 marked complete, v0.28.3 scope adjusted.
+
+### ICD Runner
+
+No runner changes — websocket events are not testable via HTTP. All
+existing tests remain at 469/469 (100%).
+
+### Fixed
+
+#### WebSocket Workflow Event Delivery (cs1)
+- `workflow.advanced`, `workflow.completed`, `workflow.claimed` were
+ emitted via room-scoped `Bus.Publish()`, but the room subscription
+ system is not wired (no client-side subscribe/unsubscribe). These
+ events **never reached WebSocket clients**.
+- Converted all three to `SendToUser()` per channel participant,
+ matching the pattern used by `message.created` and
+ `workflow.assigned`.
+- `workflow.claimed` payload gains `channel_id` field (needed to look
+ up participants; also useful for frontend navigation).
+- `workflow_assignments.go`: consolidated the duplicate
+ `channel_id` query (was queried separately for WS and notification).
+
+### Added
+
+#### JS Dependency Audit (cs2)
+- `docs/JS-DEPENDENCY-AUDIT.md`: full map of the frontend codebase.
+ 47 files, 25K lines, 431 top-level globals, 143 dynamic onclick
+ handlers. Documents the complete script load order per surface,
+ core global dependency graph (`App` → 20 files, `API` → 36 files,
+ `UI` → 32 files, `Events` → 9 files), IIFE vs bare global
+ classification, inline handler inventory, cross-file call graph,
+ and a 4-phase decomposition strategy (core extract → onclick
+ migration → ES modules → template handler shim).
+
+#### Core Four IIFE Extraction (cs3)
+- `api.js`: Wrapped in IIFE. `BASE` and `_storageKey` are now private
+ (were leaking as implicit globals). Single export: `window.API`.
+- `events.js`: Wrapped in IIFE. Replaced `_storageKey` localStorage
+ coupling with `API.accessToken` (eliminates cross-file private
+ dependency). Single export: `window.Events`.
+- `app-state.js`: Wrapped in IIFE. `resolveCapabilities()` now private.
+ Exports: `window.App`, `window.fetchModels`.
+- `ui-primitives.js`: Wrapped in IIFE. `updateTabArrows()` now private
+ (zero external callers). 17 explicit `window.*` exports with manifest
+ comment. Previously all 18 declarations leaked as implicit globals.
+
+#### Tier 2 IIFE Extraction (cs4)
+- `ui-format.js`: Wrapped in IIFE. 11 explicit exports (7 cross-file +
+ 4 onclick handlers), 9 privatized (`_highlightMentions`, `_formatMarked`,
+ `_formatBasic`, `_unwrapMarkdownFence`, `_decodeHTML`, `_looksLikeHTML`,
+ `_hasPreviewContent`, `_extractLastHTMLBlock`, `_livePreviewTimer`).
+- `ui-core.js`: Wrapped in IIFE. 3 exports (`UI`, `renderPersonaForm`,
+ `toggleSummarizedHistory`), 3 privatized (`avatarHTML`,
+ `assistantAvatarURI`, `_isSummaryMessage`).
+- `pages.js`: Wrapped in IIFE. 2 exports (`Pages`, `_val`), 6 privatized
+ (`_pageData`, `_show`, `_hide`, `_toast`, `_parseJSON`, `_api`).
+
+#### Tier 3 IIFE Extraction — Leaf Modules (cs5)
+- 11 files wrapped: `channel-models.js`, `chat-pane.js`, `notifications.js`,
+ `model-selector.js`, `file-tree.js`, `tokens.js`, `code-editor.js`,
+ `note-editor.js`, `drag-resize.js`, `pane-container.js`, `user-menu.js`.
+- 13 total explicit exports across all 11 files.
+- 17 functions privatized (e.g. `_shortName`, `_timeAgo`, `_ftFileIcon`,
+ `_ceFileIcon`, `_ceDetectLanguage`, `dismissContextWarning`, `_clientPos`,
+ 10 PaneContainer internals).
+
+#### Small File IIFE Wrap (cs6)
+- 7 files: `admin-surfaces.js`, `extensions.js`, `memory-ui.js`,
+ `notification-prefs.js`, `persona-kb.js`, `repl.js`, `ui-settings.js`.
+- `ui-settings.js` has no standalone exports (extends `UI` via
+ `Object.assign`); IIFE scopes its closure only.
+- 3 functions privatized (`_loadSurfaceList`, `_debounce`,
+ `savePersonaKBs` — zero external callers).
+
+#### Chat + App Entry Point IIFE Wrap (cs7)
+- `chat.js`: Wrapped in IIFE. 22 explicit exports (ChatInput + 21
+ functions), 7 privatized (`_typingTimer`, `_emitTyping`,
+ `_saveChatModel`, `_restoreChatModel`, `_cleanStaleChatModel`,
+ `updateChatTokenCount`, `_autoNamePending`).
+- `app.js`: Wrapped in IIFE. 6 exports (`handleLogin`, `handleRegister`,
+ `handleLogout`, `switchAuthTab`, `startApp`, `initBanners`),
+ 13 privatized. `init()` self-invokes via `DOMContentLoaded` — not
+ exported. `handleLogout` overrides the base.html fallback on chat,
+ editor, and notes surfaces.
+
+#### Clean Tier 4 Wraps (cs8)
+- `settings-handlers.js`: Wrapped in IIFE. 11 explicit exports + 4
+ self-assigned `window.*` functions (preserved from original). 13
+ privatized.
+- `note-graph.js`: Wrapped in IIFE. 6 exports, 17 privatized.
+- `debug.js`: Wrapped in IIFE. 8 exports (all onclick-referenced from
+ base.html template), 1 privatized.
+- `panels.js`: Wrapped in IIFE. 6 exports, 4 privatized.
+
+#### Heavy Page IIFE Wraps — All Remaining (cs9)
+- `files.js`: 18 exports (15 cross-file + 3 onclick), 18 privatized.
+- `admin-handlers.js`: 30 exports (28 cross-file + 2 onclick), 10
+ privatized. Previously misclassified as IIFE — was actually bare
+ globals.
+- `notes.js`: 12 exports (9 cross-file + 3 onclick), 31 privatized.
+- `ui-admin.js`: 3 exports (`ADMIN_SECTIONS`, `ADMIN_LABELS`,
+ `ADMIN_LOADERS`), 1 privatized. Also extends `UI` via
+ `Object.assign` (28 onclick handlers are UI methods, not standalone).
+- `projects-ui.js`: 48 exports (29 cross-file + 19 onclick), 30
+ privatized. Largest file in the codebase (78 top-level globals
+ reduced to 48 explicit exports).
+
+**Frontend decomposition Phase 1 complete.** All 47 JS files are now
+IIFE-wrapped with explicit `window.*` exports. Zero implicit globals
+remain. The `_storageKey` cross-file coupling between `api.js` and
+`events.js` is eliminated. ~168 functions privatized across the
+codebase.
+
+
+## [0.28.2] — 2026-03-13
+
+### Summary
+
+Full ICD audit across all remaining domains. Every ICD document traced
+against Go handlers, stores (PG + SQLite), and the ICD test runner.
+Eight changesets (cs0–cs7), 469/469 runner tests passing.
+
+### Added
+
+#### Go Integration Tests
+- `projects_test.go`: 14 tests — CRUD shapes, 201 status, envelope,
+ admin list, auth, isolation.
+- `workspace_test.go`: 11 tests — list empty/data envelope, shape,
+ root_path not exposed, user isolation, GET by ID, 404, 403, git
+ credentials empty envelope, auth required.
+- `profile_test.go`: GET profile shape, PUT profile, duplicate email
+ 409, settings envelope, password change (success, wrong current 401,
+ too short 400), avatar delete.
+- Knowledge tests: document status polling, document delete, update
+ empty body 400, permission denial.
+
+#### ICD Runner Tests (v0.28.2.4)
+- Projects: rewritten from 9 → 19 tests (full CRUD shapes,
+ channel/KB/note association lifecycles, files shape, isolation,
+ admin list).
+- Knowledge: document status, document delete, permission, team-scoped
+ KB creation.
+- Profile: all 7 endpoints exercised.
+- Workspaces: envelope, shape, isolation, git operations.
+- Notifications: envelope, preferences, type enum.
+
+### Fixed
+
+#### P0 — Security
+- `SetDiscoverable` on knowledge bases had no authorization check.
+ Added `loadAndAuthorize` + owner/admin gate + audit log (cs2).
+
+#### P0 — Response Shape
+- `GET /settings` returned bare object → wrapped in
+ `{"settings": {...}}` (cs5).
+- `GET /workspaces` returned bare array → `{"data": [...]}` (cs6).
+- `GET /git-credentials` returned bare array → `{"data": [...]}` (cs6).
+- `GET .../git/log` returned bare array → `{"data": [...]}` + nil
+ slice guard (cs6).
+- `ListByProject` files returned `{"files": null}` → `{"files": []}` (cs7).
+- `GET /notifications/preferences` missing `"preferences"` key in
+ envelope (cs0).
+
+#### P0 — Data Integrity
+- `ListTeamProviderModels` response struct missing `Type` field —
+ embedding models indistinguishable from chat models, causing Venice
+ 400 on completion (cs7).
+
+#### Notification System
+- Implemented `memory.extracted` notification (was missing hook in
+ memory extractor) (cs0).
+- Implemented `user.mentioned` persisted notification (was WS-only) (cs0).
+- Implemented `workflow.claimed` persisted notification (was WS-only) (cs0).
+- Removed dead `NotifTypeProjectInvite` constant (cs0).
+
+#### ICD Runner (v0.28.2.4)
+- SSE parser: extract content from OpenAI-format
+ `choices[0].delta.content` — was checking top-level `evt.content`.
+ All three provider tiers (global, team, BYOK) were affected (cs7).
+- `pickCheapestChat` excludes embedding/image models by type and ID
+ pattern (cs7).
+- Model ID references use `(m.model_id || m.id)` fallback for team
+ provider models (cs7).
+
+### Changed
+
+#### ICD Documents Corrected
+- `notifications.md`: object shape, query params, response envelopes,
+ WS events. Type enum synced (removed aspirational, added implemented).
+- `knowledge.md`: KB object shape (6 field mismatches), search envelope
+ (`data` not `results`), search result fields, status progression
+ (`extracting` step), file type support, auth annotations.
+- `profile.md`: avatar API (multipart → JSON base64), response shapes
+ for all 7 endpoints, auth annotations, field table. Added
+ `last_login_at` to `profileResponse` + dialect-safe time scan.
+- `workspaces.md`: workspace object shape (`indexing_enabled`, `git_*`,
+ `total_bytes` not `storage_bytes`, `owner_type` full enum), file
+ entry shape, git status/log/commit shapes, archive format, auth
+ annotations.
+- `projects.md`: full rewrite — 6 categories of drift fixed (ghost
+ fields removed, 6 missing fields added, create/update shapes
+ corrected, association objects documented, `omitempty` on computed
+ counts noted).
+- `notes.md`: verified clean.
+- `memory.md`: verified clean (audited v0.28.0).
+
+#### Dead Code Removal
+- `ListGlobal`/`ListForTeam` on KnowledgeBaseStore — interface + PG +
+ SQLite (cs4).
+- `CreateKB` moved team role check from raw `database.DB.QueryRow` to
+ `stores.Teams.IsTeamAdmin`; removed `database` import (cs4).
+- Stale `/avatar` route aliases removed from integration test harness (cs5).
+- `ListDiscoverableKBs` normalized to use `toKBResponse()` (cs3).
+
+
+## [0.28.1] — 2026-03-12
+
+### Summary
+
+Surfaces ICD audit — first domain audit pass. Corrected 6 ICD
+discrepancies and hardened the surface install path.
+
+### Fixed
+- ICD `surfaces.md` corrected: field name, archive format, response
+ shape (6 discrepancies).
+- Surface ID slug validation + `extractableRelPath` install hardening
+ (path traversal defense).
+
+### Added
+- 19 E2E surface CRUD tests in ICD runner (install, enable/disable,
+ delete, error paths).
+- 22 handler-level tests + 14 store-level tests (PG + SQLite).
+
+### Changed
+- CI timeout increased 8m → 12m for PG integration tests.
+
+
+## [0.28.0] — 2026-03-12
+
+### Summary
+
+Platform polish arc kickoff. Memory ICD audit — first domain to complete
+the full trace-and-test cycle.
+
+### Changed
+- `memory.md` ICD audited and verified: object shape, scopes
+ (`user`/`persona`/`persona_user`), statuses
+ (`active`/`pending_review`/`archived`), all endpoints, admin review,
+ AI tools, background extraction, memory injection.
+- 13/13 ICD runner tests passing.
+
+
## [0.27.5] — 2026-03-11
### Summary
diff --git a/VERSION b/VERSION
index 5fe1ecc..0ba165c 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.28.2
\ No newline at end of file
+0.28.3
\ No newline at end of file
diff --git a/docs/ICD/README.md b/docs/ICD/README.md
index e81f173..6220dd5 100644
--- a/docs/ICD/README.md
+++ b/docs/ICD/README.md
@@ -1,7 +1,7 @@
# ICD-API — Chat Switchboard Backend API Contract
-**Version:** 0.28.0
-**Updated:** 2026-03-11
+**Version:** 0.28.3
+**Updated:** 2026-03-13
**Audience:** Frontend developers, integrators, API consumers
> **Organization principle:** Each file is one domain — the complete story.
diff --git a/docs/ICD/auth.md b/docs/ICD/auth.md
index bfcc2af..45cc96b 100644
--- a/docs/ICD/auth.md
+++ b/docs/ICD/auth.md
@@ -22,9 +22,15 @@ POST /auth/register
}
```
-Gated by `allow_registration` policy. Returns same shape as Login.
-A unique `handle` is auto-generated from `username` (collision-safe
-with `-2`, `-3` suffixes).
+Gated by `allow_registration` policy. A unique `handle` is
+auto-generated from `username` (collision-safe with `-2`, `-3` suffixes).
+
+**Response (201):** Same token shape as Login. If the user requires admin
+approval (`is_active` = false), returns `201` with:
+
+```json
+{ "message": "Account created but requires admin approval", "user_id": "uuid" }
+```
### Login
@@ -36,12 +42,14 @@ POST /auth/login
{ "username": "jdoe", "password": "..." }
```
-**Response:**
+**Response (200):**
```json
{
"access_token": "eyJ...",
"refresh_token": "opaque-string",
+ "token_type": "Bearer",
+ "expires_in": 900,
"user": {
"id": "uuid",
"username": "jdoe",
@@ -49,10 +57,7 @@ POST /auth/login
"display_name": "Jane Doe",
"handle": "jdoe",
"role": "user",
- "auth_source": "builtin",
- "avatar_url": "/api/v1/profile/avatar?v=1234",
- "created_at": "...",
- "last_login": "..."
+ "auth_source": "builtin"
}
}
```
diff --git a/docs/ICD/enums.md b/docs/ICD/enums.md
index 5826f10..c714755 100644
--- a/docs/ICD/enums.md
+++ b/docs/ICD/enums.md
@@ -169,6 +169,7 @@ All enum values used across the API. Definitive source of truth.
| Value | Description |
|-------|-------------|
+| `queued` | Waiting to execute |
| `running` | Currently executing |
| `completed` | Finished successfully |
| `failed` | Error during execution |
diff --git a/docs/ICD/websocket.md b/docs/ICD/websocket.md
index 7f2d8b6..95a4fda 100644
--- a/docs/ICD/websocket.md
+++ b/docs/ICD/websocket.md
@@ -1,6 +1,8 @@
# WebSocket Protocol
-Real-time bidirectional event bus. Single connection per client.
+Real-time event delivery. Single connection per client. All event
+delivery uses targeted `SendToUser` — events are pushed to every
+WebSocket connection belonging to the recipient user.
### Connection
@@ -9,83 +11,313 @@ ws://{host}{BASE_PATH}/ws?token={access_token}
```
**Lifecycle:**
-- Server sends `ping` every 54 seconds
+- Server sends WebSocket-level `ping` frames every 54 seconds
- Client must respond with `pong` within 60 seconds or disconnection
-- Max message size: 4 KB
-- Write deadline: 10 seconds
+- Max inbound message size: 4 KB
+- Write deadline: 10 seconds per frame
+- Connection ID: `{user_id}-{timestamp}` (internal, not exposed to client)
### Event Envelope
-All messages are JSON:
+All messages are JSON. The field name is `event`, not `type`:
```json
{
- "type": "event.type",
- "payload": { ... }
+ "event": "message.created",
+ "payload": { ... },
+ "ts": 1710000000000
}
```
-### Room Subscription
+| Field | Type | Description |
+|-------|------|-------------|
+| `event` | string | Dot-delimited event label |
+| `room` | string | Optional — room scope (currently unused client-side, see § Room Model) |
+| `payload` | object | Event-specific data |
+| `ts` | int64 | Unix milliseconds |
-Clients subscribe to rooms for scoped event delivery:
+### Client → Server Events
+
+The client can send JSON events to the server. Only events with
+`DirFromClient` or `DirBoth` routing are accepted; all others are
+silently dropped.
+
+**Application-level ping:**
```json
-{ "type": "subscribe", "payload": { "room": "channel:uuid" } }
-{ "type": "unsubscribe", "payload": { "room": "channel:uuid" } }
+{ "event": "ping" }
```
+Server responds with `{ "event": "pong", "ts": ... }`. This is
+separate from the WebSocket-level ping/pong frames.
+
+**Typing (human → other participants):**
+
+Client sends a `chat.typing.{channelID}` event. The server tags it
+with `SenderID` and `ConnID`, then rebroadcasts to other participants
+via `SendToUser` per channel membership.
+
+### Room Model (Planned)
+
+`JoinRoom` and `LeaveRoom` methods exist on the connection struct but
+are **not yet wired** — no client-side subscribe/unsubscribe mechanism
+is implemented. The `subscribeToBus` handler has room filtering logic
+(`if e.Room != "" && !c.rooms[e.Room]`) but since connections never
+join rooms, all room-scoped `Bus.Publish` calls are effectively
+no-ops for WebSocket delivery.
+
+Current workaround: all user-facing events use `Hub.SendToUser()`
+which bypasses room filtering. Room-scoped `Bus.Publish()` calls
+(e.g. `workflow.claimed` with `Room: assignmentID`) exist as
+forward-looking plumbing but do not reach WebSocket clients today.
+
### Event Routing Table
-| Prefix | Direction | Description |
-|--------|-----------|-------------|
-| `channel.created` | → client | New channel created |
-| `channel.updated` | → client | Channel metadata changed |
-| `channel.deleted` | → client | Channel removed |
-| `message.created` | → client | New message in subscribed channel |
-| `message.updated` | → client | Message content changed |
-| `message.deleted` | → client | Message removed |
-| `cursor.updated` | → client | Branch cursor moved |
-| `typing.start` | ↔ both | Participant started typing (payload includes `participant_id`, `participant_type`) |
-| `typing.stop` | ↔ both | Typing ended |
-| `participant.joined` | → client | Participant added to channel |
-| `participant.left` | → client | Participant removed from channel |
-| `participant.updated` | → client | Participant role changed |
-| `presence.update` | → client | Participant online/idle/offline status change |
-| `model.changed` | → client | Channel model roster changed |
-| `persona.updated` | → client | Persona metadata changed |
-| `kb.updated` | → client | Knowledge base changed |
-| `kb.document.status` | → client | Document processing status update |
-| `note.created` | → client | Note created |
-| `note.updated` | → client | Note content changed |
-| `note.deleted` | → client | Note removed |
-| `notification.new` | → client | New notification |
-| `notification.read` | → client | Notification marked read |
-| `memory.extracted` | → client | New memory extracted |
-| `memory.status` | → client | Memory approved/rejected |
-| `role.fallback` | → client | Role fallback triggered |
-| `user.mentioned` | → client | User was @mentioned in a channel |
-| `workflow.assigned` | → client | Workflow stage assigned to team/user |
-| `workflow.claimed` | → client | Workflow assignment claimed |
-| `workflow.advanced` | → client | Workflow stage advanced |
-| `workflow.completed` | → client | Workflow instance completed |
-| `workspace.updated` | → client | Workspace state changed |
-| `project.updated` | → client | Project metadata changed |
-| `extension.updated` | → client | Extension config changed |
-| `settings.updated` | → client | Global settings changed |
-| `user.updated` | → client | User profile changed |
-| `file.created` | → client | File uploaded or generated (§17b.4) |
-| `file.deleted` | → client | File removed |
+Direction is determined by `events.routeTable` in `server/events/types.go`.
+Prefix matching: longest matching prefix wins; unmatched labels default
+to `DirLocal` (server-only, never crosses the wire).
-Direction: `→ client` = server-to-client only, `← client` = client-to-server
-only, `↔ both` = bidirectional.
+| Label | Direction | Delivery | Description |
+|-------|-----------|----------|-------------|
+| `message.created` | → client | `SendToUser` per channel | New message in channel |
+| `message.updated` | → client | `SendToUser` | Message content changed |
+| `message.deleted` | → client | `SendToUser` | Message removed |
+| `channel.created` | → client | `SendToUser` | New channel created |
+| `channel.updated` | → client | `SendToUser` | Channel metadata changed |
+| `channel.deleted` | → client | `SendToUser` | Channel removed |
+| `channel.member.*` | → client | `SendToUser` | Participant added/removed/role changed |
+| `typing.start` | → client | `SendToUser` | AI persona started generating |
+| `typing.stop` | → client | `SendToUser` | AI persona finished generating |
+| `typing.user` | → client | `SendToUser` per channel | Human typing indicator |
+| `chat.typing.*` | ↔ both | Bus (prefix match) | Legacy typing relay |
+| `user.presence` | → client | `SendToUser` | Online/offline status change |
+| `user.mentioned` | → client | `SendToUser` | User @mentioned in a channel |
+| `notification.new` | → client | `SendToUser` | New persisted notification |
+| `notification.read` | → client | `SendToUser` | Badge sync across tabs |
+| `role.fallback` | → client | Bus (Room: `admin`) | Role fallback activated (admin-targeted) |
+| `workflow.assigned` | → client | `SendToUser` per team | New assignment for team members |
+| `workflow.claimed` | → client | `SendToUser` per channel | Assignment claimed |
+| `workflow.advanced` | → client | `SendToUser` per channel | Stage advanced |
+| `workflow.completed` | → client | `SendToUser` per channel | Workflow finished |
+| `workspace.file.*` | → client | Bus | Workspace file changed |
+| `tool.call.*` | → client | `SendToUser` | Browser tool invocation |
+| `tool.result.*` | ← client | Bus | Browser tool result |
+| `system.notify` | → client | Bus | System broadcast |
+| `ping` | ← client | Direct | Application-level keepalive |
+| `pong` | → client | Direct | Response to application ping |
---
-## 19. Appendix: Enums
+## Event Payload Shapes
+
+### `message.created`
+
+User message (mention_only / DM relay):
+
+```json
+{
+ "id": "uuid",
+ "channel_id": "uuid",
+ "role": "user",
+ "content": "message text",
+ "user_id": "uuid"
+}
+```
+
+Assistant message (completion chain):
+
+```json
+{
+ "id": "uuid",
+ "channel_id": "uuid",
+ "role": "assistant",
+ "content": "response text",
+ "model": "model-id",
+ "participant_type": "persona",
+ "participant_id": "uuid",
+ "display_name": "Persona Name",
+ "avatar": "/path/to/avatar",
+ "tokens_used": 1234,
+ "chain_depth": 1
+}
+```
+
+### `typing.start` / `typing.stop`
+
+AI typing indicator (targeted via `SendToUser`):
+
+```json
+{
+ "channel_id": "uuid",
+ "participant_id": "uuid",
+ "participant_type": "persona",
+ "display_name": "Persona Name"
+}
+```
+
+### `typing.user`
+
+Human typing indicator (relayed to other channel participants):
+
+```json
+{
+ "channel_id": "uuid",
+ "user_id": "uuid",
+ "display_name": "Jane Doe"
+}
+```
+
+### `user.presence`
+
+Emitted on WebSocket connect/disconnect:
+
+```json
+{
+ "user_id": "uuid",
+ "status": "online|offline"
+}
+```
+
+Note: the DB CHECK constraint allows `online`, `away`, `offline` but
+the WebSocket hub currently only emits `online` (on connect) and
+`offline` (on last connection closed). `away` is reserved for future
+idle detection.
+
+### `user.mentioned`
+
+Targeted to the mentioned user:
+
+```json
+{
+ "channel_id": "uuid",
+ "from_user": "uuid",
+ "content": "truncated to 120 chars..."
+}
+```
+
+### `notification.new`
+
+Full notification object (same shape as REST `GET /notifications`):
+
+```json
+{
+ "id": "uuid",
+ "user_id": "uuid",
+ "type": "kb.ready",
+ "title": "Knowledge base ready",
+ "body": "optional detail",
+ "resource_type": "knowledge_base",
+ "resource_id": "uuid",
+ "is_read": false,
+ "created_at": "..."
+}
+```
+
+### `notification.read`
+
+Single notification marked read:
+
+```json
+{ "id": "uuid" }
+```
+
+Mark-all-read:
+
+```json
+{ "action": "mark_all_read" }
+```
+
+### `workflow.assigned`
+
+Targeted via `SendToUser` to each team member:
+
+```json
+{
+ "channel_id": "uuid",
+ "team_id": "uuid",
+ "stage_name": "Review",
+ "assigned_to": "uuid|empty"
+}
+```
+
+### `workflow.claimed`
+
+Targeted via `SendToUser` to all user participants in the channel:
+
+```json
+{
+ "assignment_id": "uuid",
+ "claimed_by": "uuid",
+ "channel_id": "uuid"
+}
+```
+
+### `workflow.advanced`
+
+Targeted via `SendToUser` to all user participants in the channel:
+
+```json
+{
+ "channel_id": "uuid",
+ "workflow_id": "uuid",
+ "stage": "stage_key",
+ "stage_name": "Stage Name"
+}
+```
+
+### `workflow.completed`
+
+Targeted via `SendToUser` to all user participants in the channel:
+
+```json
+{
+ "channel_id": "uuid",
+ "workflow_id": "uuid",
+ "stage": "final_stage_key"
+}
+```
+
+### `role.fallback`
+
+Published to bus with `Room: "admin"`:
+
+```json
+{
+ "role": "primary",
+ "operation": "chat|embedding",
+ "primary_model": "model-a",
+ "fallback_model": "model-b",
+ "message": "Role \"primary\" primary (model-a) failed — using fallback (model-b)"
+}
+```
+
+### `tool.call.*`
+
+Browser tool invocation (targeted via `SendToUser`):
+
+```json
+{
+ "tool_name": "...",
+ "call_id": "...",
+ "input": { ... }
+}
+```
+
+Client responds with `tool.result.*` containing the execution result.
+
+---
+
+## Appendix: Enums
### Channel Types
-`direct` (single-user), `group` (multi-user/multi-model), `workflow` (staged)
+`direct` (single-user), `dm` (human-to-human, AI mention-only),
+`group` (multi-user/multi-model), `channel` (named persistent),
+`workflow` (staged), `service` (autonomous task execution)
+
+### Channel AI Modes
+
+`auto` (default), `mention_only` (default for `dm`), `off`
### Participant Types
@@ -93,11 +325,15 @@ only, `↔ both` = bidirectional.
### Participant Roles
-`owner`, `member`, `observer`
+`owner`, `member`, `observer`, `visitor`
### Presence Statuses
-`online`, `idle`, `offline`
+`online`, `away`, `offline`
+
+DB CHECK constraint: `online`, `away`, `offline`. Runtime: only
+`online` and `offline` are emitted by the WebSocket hub. `away` is
+reserved for future idle-timeout detection.
### Message Roles
@@ -129,11 +365,11 @@ only, `↔ both` = bidirectional.
### Memory Scopes
-`user`, `persona`
+`user`, `persona`, `persona_user`
### Memory Statuses
-`pending`, `approved`, `rejected`
+`active`, `pending_review`, `archived`
### Workspace Owner Types
@@ -141,11 +377,11 @@ only, `↔ both` = bidirectional.
### Workspace Statuses
-`active`, `archived`
+`active`, `archived`, `deleting`
### Index Statuses
-`pending`, `indexing`, `ready`, `error`
+`pending`, `indexing`, `ready`, `error`, `skipped`
### KB Document Statuses
@@ -180,6 +416,18 @@ only, `↔ both` = bidirectional.
See [enums.md](enums.md#notification-types) for full descriptions.
+### Workflow Assignment Statuses
+
+`unassigned`, `claimed`, `completed`
+
+### Task Run Statuses
+
+`queued`, `running`, `completed`, `failed`, `budget_exceeded`, `cancelled`
+
+### Workflow Instance Statuses
+
+`active`, `completed`, `stale`, `cancelled`
+
### Git Auth Types
`https_pat`, `https_basic`, `ssh_key`
diff --git a/docs/JS-DEPENDENCY-AUDIT.md b/docs/JS-DEPENDENCY-AUDIT.md
new file mode 100644
index 0000000..34a336a
--- /dev/null
+++ b/docs/JS-DEPENDENCY-AUDIT.md
@@ -0,0 +1,348 @@
+# Frontend JS Dependency Audit
+
+**Version:** v0.28.3 cs2
+**Date:** 2026-03-13
+**Purpose:** Map every global, cross-file dependency, and init sequence
+to prepare for module extraction.
+
+---
+
+## Summary
+
+| Metric | Value |
+|--------|-------|
+| JS files | 47 |
+| Total lines | 25,056 |
+| Total bytes | 1,103,572 (~1.1 MB) |
+| Top-level globals (original) | 431 |
+| IIFE-wrapped files | 47 (all) |
+| Explicit `window.*` exports | ~215 |
+| Functions privatized | ~168 |
+| Implicit globals remaining | 0 |
+| Vendor libs | 3 (marked.min.js, purify.min.js, codemirror.bundle.js) |
+| Inline `onclick` handlers (templates) | ~50 unique functions |
+| Dynamic `onclick` in JS (innerHTML) | 143 total occurrences |
+
+---
+
+## Architecture: IIFE Modules (Post-Decomposition)
+
+No module system (no ES modules, no bundler, no AMD). Every file is a
+`
+```
+
+Import graph follows the dependency order already established by
+`