Changeset 0.28.3 (#187)

This commit is contained in:
2026-03-14 12:30:57 +00:00
parent b2c03be001
commit f68a58b073
46 changed files with 1589 additions and 107 deletions

View File

@@ -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.

View File

@@ -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"
}
}
```

View File

@@ -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 |

View File

@@ -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`

348
docs/JS-DEPENDENCY-AUDIT.md Normal file
View File

@@ -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
`<script>` tag loaded in dependency order. All 47 files are now wrapped
in IIFEs with explicit `window.*` exports:
```js
(function() {
'use strict';
// ... all code ...
window.MyGlobal = MyGlobal;
})();
```
**Phase 1 complete.** Zero implicit globals remain. ~168 functions
privatized. The next phase is onclick → addEventListener migration
(143 dynamic handlers in JS innerHTML + ~50 in Go templates), which
is the prerequisite for ES module conversion.
### IIFE Files
| File | Clean? | Leaks |
|------|--------|-------|
| `admin-handlers.js` | ✅ | None (all via inline onclick from globals) |
| `admin-scaffold.js` | ✅ | None |
| `editor-surface.js` | ✅ | None |
| `knowledge-ui.js` | ✅ | None |
| `pages-splash.js` | ✅ | None |
| `task-admin.js` | ❌ | `window._loadAdminTasks` |
| `task-settings.js` | ❌ | `window._createFromTemplate`, `window._loadSettingsTasks` |
| `task-sidebar.js` | ❌ | `window.TaskSidebar` |
| `tools-toggle.js` | ✅ | None |
| `workflow-admin.js` | ❌ | 5 functions (`_loadAdminWorkflows`, `_wfCreate`, `_wfOpen`, `_wfEditStage`, `_wfDeleteStage`) |
| `workflow-api.js` | ✅ | None (registers on App) |
| `workflow-queue.js` | ❌ | `window.WorkflowQueue` |
---
## Script Load Order
### base.html (shared — every surface)
```
1. app-state.js → const App = { ... }
2. api.js → const API = { ... }
3. (inline) → window.__BASE__, auth bootstrap
4. events.js → const Events = { ... }
5. ui-primitives.js → const UI = { ... }
6. vendor/marked.min.js
7. vendor/purify.min.js
8. ui-format.js → formatMessage(), toggleCodeCollapse(), etc.
9. ui-core.js → renderChatList(), renderMessages(), etc.
10. pages.js → const Pages = { ... }
11. user-menu.js → const UserMenu = { ... }
12. model-selector.js → const ModelSelector = { ... }
13. file-tree.js → const FileTree = { ... }
14. code-editor.js → const CodeEditor = { ... }
15. note-editor.js → const NoteEditor = { ... }
16. drag-resize.js → const DragResize = { ... }
17. pane-container.js → const PaneContainer = { ... }
18. (inline) → Theme.init()
19. surfaces/{surface}/js/main.js → surface CSS + scaffold
```
### chat.html (25 additional scripts + 1 vendor)
```
vendor/codemirror/codemirror.bundle.js → window.CM (optional, onerror fallback)
20. extensions.js → const Extensions = { ... }
21. panels.js → const PanelRegistry = { ... }
22. ui-settings.js → settings rendering (bare globals)
23. ui-admin.js → admin rendering (bare globals)
24. tokens.js → token counting (bare globals)
25. notes.js → notes UI (bare globals)
26. note-graph.js → graph visualization (bare globals)
27. files.js → file upload/management (bare globals)
28. tools-toggle.js → tool enable/disable (IIFE)
29. knowledge-ui.js → KB picker (IIFE)
30. memory-ui.js → memory panel (bare globals)
31. persona-kb.js → persona KB bindings (bare globals)
32. projects-ui.js → project management (bare globals, 78 top-level decls)
33. channel-models.js → const ChannelModels = { ... }
34. notification-prefs.js → notification preferences (bare globals)
35. notifications.js → const Notifications = { ... }
36. chat.js → const ChatInput = { ... } + 29 top-level functions
37. settings-handlers.js → settings form handlers (bare globals, 24 decls)
38. admin-handlers.js → admin form handlers (IIFE, 40 internal functions)
39. workflow-api.js → workflow REST helpers (IIFE)
40. workflow-admin.js → workflow admin UI (IIFE, 5 window.* leaks)
41. workflow-queue.js → workflow queue sidebar (IIFE, window.WorkflowQueue)
42. task-sidebar.js → task sidebar (IIFE, window.TaskSidebar)
43. app.js → init(), startApp(), event listeners
44. debug.js → debug console (bare globals)
45. repl.js → REPL (bare globals)
```
### admin.html (14 additional)
Loads: ui-settings, ui-admin, admin-handlers, settings-handlers,
knowledge-ui, persona-kb, memory-ui, notifications, files,
admin-scaffold, workflow-api, workflow-admin, task-admin,
admin-surfaces.
### notes.html (14 additional)
Loads: chat, files, channel-models, tokens, extensions, notes,
note-graph, knowledge-ui, persona-kb, memory-ui, notifications,
panels, projects-ui, app.
### settings.html (9 additional)
Loads: ui-settings, settings-handlers, task-settings, knowledge-ui,
persona-kb, memory-ui, notifications, notification-prefs,
channel-models.
### editor.html (4 additional + 1 vendor)
Loads: vendor/codemirror, chat-pane, editor-surface, app.
Lightest surface — fewest dependencies.
---
## Core Globals (Dependency Roots)
These are referenced by 10+ other files. They form the "root set" that
must be extracted first.
| Global | Defined in | Referenced by | Role |
|--------|-----------|---------------|------|
| `App` | app-state.js | 20 files | State container (chats, models, user, settings, presence) |
| `API` | api.js | 36 files | REST client (all HTTP calls) |
| `UI` | ui-core.js | 32 files | DOM helpers (toast, confirm, modal, sidebar, theme) |
| `Events` | events.js | 9 files | Pub/sub event bus (WebSocket bridge) |
### Secondary Globals (39 references)
| Global | Defined in | Referenced by | Role |
|--------|-----------|---------------|------|
| `Pages` | pages.js | Templates + 3 files | Page-level actions (login, admin forms) |
| `ChannelModels` | channel-models.js | 4 files | Model roster per channel |
| `ChatInput` | chat.js | 3 files | Message input state + send |
| `Notifications` | notifications.js | Templates + 2 files | Bell dropdown, badge count |
| `ChatPane` | chat-pane.js | 3 files | Embedded chat component (editor) |
| `CM` | vendor/codemirror.bundle.js | 5 files | CodeMirror 6 editor (optional, graceful fallback) |
| `PanelRegistry` | panels.js | Templates + 1 file | Side panel management |
### Leaf Globals (12 references, mostly self-contained)
ModelSelector, FileTree, CodeEditor, NoteEditor, DragResize,
PaneContainer, UserMenu, WorkflowQueue, TaskSidebar, Extensions.
---
## Inline Handler Problem
143 `onclick=` patterns in JS files (innerHTML-generated HTML) plus
~50 in Go templates. These reference bare globals that **must remain
on `window`** or the handlers break. This is the primary blocker for
ES module migration.
**Top offenders (dynamic onclick in JS):**
| File | Count | Examples |
|------|-------|---------|
| ui-admin.js | 28 | Admin list row actions |
| projects-ui.js | 26 | Project CRUD, channel/KB/note associations |
| ui-core.js | 25 | Chat list, sidebar, context menus |
| ui-format.js | 10 | Code block copy/preview/download |
| ui-settings.js | 8 | Settings form toggles |
| notifications.js | 8 | Notification row actions |
**Template onclick (must remain global):**
UI.toggleSidebar, UI.toggleSidebarSection, UI.switchTeamTab,
Notifications.toggleDropdown, PanelRegistry.toggle, PanelRegistry.closeAll,
Pages.doLogin, Pages.saveSettings, Pages.saveProvider, Pages.saveTeam,
handleLogin, handleRegister, switchAuthTab, newChat, newFolder,
newChannelOrDM, sendMessage, createProject, closeLightbox,
toggleSidePanelFullscreen, plus ~15 more Pages.* admin form handlers.
---
## Cross-File Call Graph (Simplified)
```
app-state.js (App)
↑ read/write by 20 files
api.js (API)
↑ called by 36 files
events.js (Events)
↑ subscribed by 9 files
├── chat.js (typing, message.created, presence)
├── notifications.js (notification.new/read)
├── extensions.js (tool.call/result)
├── repl.js (debug events)
└── app.js (role.fallback banner)
ui-primitives.js (UI)
↑ called by 32 files
├── toast(), confirm(), modal()
├── toggleSidebar(), toggleSidebarSection()
└── theme, kbd shortcuts
ui-format.js (formatMessage, etc.)
↑ called by ui-core.js, chat.js, notes.js, projects-ui.js
├── depends on: marked.min.js, purify.min.js
ui-core.js (renderChatList, renderMessages, etc.)
↑ called by chat.js, app.js, projects-ui.js
├── depends on: App, API, UI, ui-format
chat.js (ChatInput, sendMessage, selectChat, loadChats)
↑ called by app.js, templates
├── depends on: App, API, Events, UI, ui-core, ui-format,
│ ChannelModels, tokens, files
app.js (init, startApp)
↑ entry point — called by inline <script> in base.html
├── depends on: everything above + surface-specific modules
```
---
## Decomposition Strategy
### Phase 1: Extract Core Four (no bundler required)
Move `App`, `API`, `Events`, `UI` to a pattern where they're defined
as proper singletons with explicit exports. No ES modules yet — just
clean up the global registration so each file has a single `window.X`
export with a documented interface.
**Why no bundler yet:** 143 dynamic onclick handlers + 50 template
onclick handlers require globals on `window`. A bundler would need
either (a) a global shim layer or (b) converting all handlers to
`addEventListener`. Option (b) is the right answer but it's ~200
call sites across 16 files + 10 templates — too much churn for one
changeset.
### Phase 2: onclick → addEventListener migration
Convert dynamic HTML generation from:
```js
`<button onclick="deleteItem('${id}')">Delete</button>`
```
to:
```js
const btn = document.createElement('button');
btn.textContent = 'Delete';
btn.addEventListener('click', () => deleteItem(id));
```
This eliminates the global requirement. Target the top 6 files first
(ui-admin, projects-ui, ui-core, ui-format, ui-settings, notifications)
which account for 105 of 143 occurrences.
Template onclick handlers stay — Go templates can't do addEventListener.
These become the only remaining reason for `window.*` exports.
### Phase 3: ES Module conversion
Once onclick handlers are gone from JS, files can become ES modules:
```html
<script type="module" src="js/app.js"></script>
```
Import graph follows the dependency order already established by
`<script>` tag ordering. Vendor libs (marked, purify, codemirror) stay
as classic scripts since they self-register on `window`.
### Phase 4: Template handler shim
Create a thin `window.handlers = {}` object that ES modules register
into. Template onclick handlers call `handlers.doLogin()` etc. This
is the final bridge — once templates move to client-side rendering
(extension surface SDK), this shim disappears.
---
## Files by Decomposition Priority
### Tier 1 — Core (extract first, most dependents)
| File | Lines | Globals | Dependents | Notes |
|------|-------|---------|------------|-------|
| app-state.js | 96 | 3 | 20 | Clean singleton, easy extract |
| api.js | 1,159 | 3 | 36 | Large but self-contained |
| events.js | 261 | 1 | 9 | Clean singleton |
| ui-primitives.js | 1,316 | 18 | 32 | Large, many utility functions |
### Tier 2 — Rendering (depends on Tier 1)
| File | Lines | Globals | Dependents | Notes |
|------|-------|---------|------------|-------|
| ui-format.js | 573 | 20 | 4 | Markdown + code blocks |
| ui-core.js | 1,644 | 6 | 3 | Chat list, messages, sidebar |
| pages.js | 382 | 8 | Templates | Page-level orchestration |
### Tier 3 — Feature modules (leaf nodes, mostly self-contained)
| File | Lines | Notes |
|------|-------|-------|
| channel-models.js | 425 | Clean object literal |
| chat-pane.js | 143 | Clean object literal |
| notifications.js | 390 | Clean singleton, IIFE candidate |
| model-selector.js | 191 | Clean object literal |
| file-tree.js | 295 | Clean object literal |
| tokens.js | 143 | 4 globals, minimal deps |
### Tier 4 — Heavy pages (most onclick handlers, refactor last)
| File | Lines | onclick | Notes |
|------|-------|---------|-------|
| ui-admin.js | 1,922 | 28 | Admin rendering, deeply coupled |
| projects-ui.js | 1,794 | 26 | 78 top-level globals (!), biggest mess |
| ui-core.js | 1,644 | 25 | Already in Tier 2, but onclick heavy |
| ui-settings.js | 1,011 | 8 | Settings rendering |
| settings-handlers.js | 1,064 | 0 | But 24 globals called by ui-settings |
| admin-handlers.js | 952 | 2 | IIFE but 40 internal functions |

View File

@@ -24,8 +24,8 @@ v0.9.xv0.27.5 Foundation → Extensions → Surfaces → Auth ✅
v0.28.0 Platform Polish
├─ v0.28.1 Surfaces ICD audit ✅
├─ v0.28.2 ICD audit — all domains
├─ v0.28.3 FE decomp + SDK prep
├─ v0.28.2 ICD audit — all domains
├─ v0.28.3 ICD close-out + FE decomp prep
├─ v0.28.4 Security tier (red team)
├─ v0.28.5 Infrastructure
│ (virtual scroll, KB auto-inject,
@@ -67,7 +67,7 @@ Audit arc, frontend decomposition, security, and infrastructure improvements.
- [x] 22 handler-level tests + 14 store-level tests (PG + SQLite)
- [x] CI timeout 8m → 12m for PG integration tests
### v0.28.2 — ICD Audit: All Domains
### v0.28.2 — ICD Audit: All Domains
Full audit of every ICD document against code. Goal: ICD becomes the single
source of truth. After this version, the workflow is ICD-first — update the
contract before changing code.
@@ -77,6 +77,8 @@ contract before changing code.
ICD → fix code → fix runner → CI green. Final pass picks up straggling
failures across all domains.
**Final score: 469/469 (100%)**
**Completed audits:**
*Notifications (cs0):*
@@ -128,32 +130,53 @@ failures across all domains.
not `files`), archive format query param, auth annotations, all response shapes
- [x] P0 fix: `GET /workspaces` returns bare array → `{"data": [...]}`
- [x] P0 fix: `GET /git-credentials` returns bare array → `{"data": [...]}`
- [x] Fix: `GET .../git/log` returns bare array → `{"data": [...]}` + nil slice guard
- [x] Fix: `GET .../git/log` returns bare array → `{"data": [...]}` + nil slice guard
- [x] New `workspace_test.go`: List empty envelope, list with data + shape, root_path
not exposed, user isolation, GET by ID shape, not found 404, forbidden 403,
git-credentials empty envelope, auth required (11 tests)
**Remaining audits:**
*Projects (cs7):*
- [x] `projects.md` full rewrite: 6 categories of drift (ghost fields, missing fields,
request shapes, association objects, `omitempty` on computed counts)
- [x] 14 new Go tests: CRUD shapes, 201 status, envelope, admin list, auth, isolation
- [x] P0 nil guard: `ListByProject` files `null``[]`
- [x] P0: `ListTeamProviderModels` missing `Type` field — Venice 400 fix
- [x] ICD runner projects rewritten 9 → 19 tests
- [x] ICD runner tier-providers: SSE parser, model exclusion, ID fallback fixes
| Priority | ICD | Known failures | Notes |
|----------|-----|----------------|-------|
| 1 | `projects.md` | 0 | Passed clean, needs full trace |
| 2 | `websocket.md` | — | Document event shapes only (not testable in runner) |
### v0.28.3 — ICD Close-out + Frontend Decomposition Prep
ICD straggler sweep (rolled in from v0.28.2 tail) and frontend
decomposition groundwork.
- [ ] Notification handler + store tests (PG + SQLite)
- [ ] WebSocket event contract audit: document `message.created`, `workflow.advanced`,
`presence.changed`, `typing`, `workflow.assigned` event shapes in ICD
- [ ] Final straggler pass: run full ICD runner, fix any remaining failures
**ICD close-out (cs0):**
- [x] `websocket.md` full rewrite: event envelope field (`event` not `type`),
payload shapes for 11 event types, routing table from code, room model
documented as planned-not-implemented
- [x] `auth.md` corrected: login response shape (added `token_type`/`expires_in`,
removed phantom fields), register admin-approval path
- [x] `enums.md` corrected: added missing `queued` task run status
- [x] Presence status gap documented (DB allows `away`, runtime emits only
`online`/`offline`)
- [x] VERSION, CHANGELOG, ROADMAP updated
### v0.28.3 — Frontend Decomposition + ICD Audit SDK Prep
Frontend JS decomposition and ICD runner hardening. Prepares the codebase
for the SDK layer by untangling the current 15-file global soup.
**WebSocket delivery fix (cs1):**
- [x] `workflow.claimed`, `workflow.advanced`, `workflow.completed` use room-scoped
`Bus.Publish()` but rooms are never joined — events never reach WebSocket
clients. Convert to `SendToUser()` per channel participant (same pattern as
`message.created`, `workflow.assigned`).
- [ ] JS dependency audit: map every `window.*` global, cross-file call, init order
- [ ] Extract shared primitives: toast, confirm, theme, API client into importable modules
**Frontend decomposition:**
- [x] JS dependency audit: `docs/JS-DEPENDENCY-AUDIT.md` — full map of 47 files,
431 globals, cross-file call graph, script load order per surface
- [x] IIFE extraction: all 47 JS files wrapped, ~168 functions privatized, zero
implicit globals remain. Explicit `window.*` exports on every file.
- [x] Cross-file coupling fix: `events.js` no longer reads `_storageKey` from
`api.js` — uses `API.accessToken` instead
- [ ] ICD runner gains test tiers: `crud`, `envelope`, `security`, `sdk`
- [ ] Runner coverage target: 100% of ICD-documented endpoints have at least one test
- [ ] Document JS module graph and init sequence
- [ ] Phase 2: onclick → addEventListener migration (143 dynamic + ~50 template handlers)
- [ ] Phase 3: ES module conversion
- [ ] Phase 4: Template handler shim
### v0.28.4 — Security Tier (ICD Runner Red Team)
New `security` tier in ICD test runner. Multi-user fixtures already exist.