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,5 +1,306 @@
# Changelog # 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 (cs0cs7), 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 ## [0.27.5] — 2026-03-11
### Summary ### Summary

View File

@@ -1 +1 @@
0.28.2 0.28.3

View File

@@ -1,7 +1,7 @@
# ICD-API — Chat Switchboard Backend API Contract # ICD-API — Chat Switchboard Backend API Contract
**Version:** 0.28.0 **Version:** 0.28.3
**Updated:** 2026-03-11 **Updated:** 2026-03-13
**Audience:** Frontend developers, integrators, API consumers **Audience:** Frontend developers, integrators, API consumers
> **Organization principle:** Each file is one domain — the complete story. > **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. Gated by `allow_registration` policy. A unique `handle` is
A unique `handle` is auto-generated from `username` (collision-safe auto-generated from `username` (collision-safe with `-2`, `-3` suffixes).
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 ### Login
@@ -36,12 +42,14 @@ POST /auth/login
{ "username": "jdoe", "password": "..." } { "username": "jdoe", "password": "..." }
``` ```
**Response:** **Response (200):**
```json ```json
{ {
"access_token": "eyJ...", "access_token": "eyJ...",
"refresh_token": "opaque-string", "refresh_token": "opaque-string",
"token_type": "Bearer",
"expires_in": 900,
"user": { "user": {
"id": "uuid", "id": "uuid",
"username": "jdoe", "username": "jdoe",
@@ -49,10 +57,7 @@ POST /auth/login
"display_name": "Jane Doe", "display_name": "Jane Doe",
"handle": "jdoe", "handle": "jdoe",
"role": "user", "role": "user",
"auth_source": "builtin", "auth_source": "builtin"
"avatar_url": "/api/v1/profile/avatar?v=1234",
"created_at": "...",
"last_login": "..."
} }
} }
``` ```

View File

@@ -169,6 +169,7 @@ All enum values used across the API. Definitive source of truth.
| Value | Description | | Value | Description |
|-------|-------------| |-------|-------------|
| `queued` | Waiting to execute |
| `running` | Currently executing | | `running` | Currently executing |
| `completed` | Finished successfully | | `completed` | Finished successfully |
| `failed` | Error during execution | | `failed` | Error during execution |

View File

@@ -1,6 +1,8 @@
# WebSocket Protocol # 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 ### Connection
@@ -9,83 +11,313 @@ ws://{host}{BASE_PATH}/ws?token={access_token}
``` ```
**Lifecycle:** **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 - Client must respond with `pong` within 60 seconds or disconnection
- Max message size: 4 KB - Max inbound message size: 4 KB
- Write deadline: 10 seconds - Write deadline: 10 seconds per frame
- Connection ID: `{user_id}-{timestamp}` (internal, not exposed to client)
### Event Envelope ### Event Envelope
All messages are JSON: All messages are JSON. The field name is `event`, not `type`:
```json ```json
{ {
"type": "event.type", "event": "message.created",
"payload": { ... } "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 ```json
{ "type": "subscribe", "payload": { "room": "channel:uuid" } } { "event": "ping" }
{ "type": "unsubscribe", "payload": { "room": "channel:uuid" } }
``` ```
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 ### Event Routing Table
| Prefix | Direction | Description | Direction is determined by `events.routeTable` in `server/events/types.go`.
|--------|-----------|-------------| Prefix matching: longest matching prefix wins; unmatched labels default
| `channel.created` | → client | New channel created | to `DirLocal` (server-only, never crosses the wire).
| `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: `→ client` = server-to-client only, `← client` = client-to-server | Label | Direction | Delivery | Description |
only, `↔ both` = bidirectional. |-------|-----------|----------|-------------|
| `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 ### 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 ### Participant Types
@@ -93,11 +325,15 @@ only, `↔ both` = bidirectional.
### Participant Roles ### Participant Roles
`owner`, `member`, `observer` `owner`, `member`, `observer`, `visitor`
### Presence Statuses ### 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 ### Message Roles
@@ -129,11 +365,11 @@ only, `↔ both` = bidirectional.
### Memory Scopes ### Memory Scopes
`user`, `persona` `user`, `persona`, `persona_user`
### Memory Statuses ### Memory Statuses
`pending`, `approved`, `rejected` `active`, `pending_review`, `archived`
### Workspace Owner Types ### Workspace Owner Types
@@ -141,11 +377,11 @@ only, `↔ both` = bidirectional.
### Workspace Statuses ### Workspace Statuses
`active`, `archived` `active`, `archived`, `deleting`
### Index Statuses ### Index Statuses
`pending`, `indexing`, `ready`, `error` `pending`, `indexing`, `ready`, `error`, `skipped`
### KB Document Statuses ### KB Document Statuses
@@ -180,6 +416,18 @@ only, `↔ both` = bidirectional.
See [enums.md](enums.md#notification-types) for full descriptions. 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 ### Git Auth Types
`https_pat`, `https_basic`, `ssh_key` `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.0 Platform Polish
├─ v0.28.1 Surfaces ICD audit ✅ ├─ v0.28.1 Surfaces ICD audit ✅
├─ v0.28.2 ICD audit — all domains ├─ v0.28.2 ICD audit — all domains
├─ v0.28.3 FE decomp + SDK prep ├─ v0.28.3 ICD close-out + FE decomp prep
├─ v0.28.4 Security tier (red team) ├─ v0.28.4 Security tier (red team)
├─ v0.28.5 Infrastructure ├─ v0.28.5 Infrastructure
│ (virtual scroll, KB auto-inject, │ (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] 22 handler-level tests + 14 store-level tests (PG + SQLite)
- [x] CI timeout 8m → 12m for PG integration tests - [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 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 source of truth. After this version, the workflow is ICD-first — update the
contract before changing code. contract before changing code.
@@ -77,6 +77,8 @@ contract before changing code.
ICD → fix code → fix runner → CI green. Final pass picks up straggling ICD → fix code → fix runner → CI green. Final pass picks up straggling
failures across all domains. failures across all domains.
**Final score: 469/469 (100%)**
**Completed audits:** **Completed audits:**
*Notifications (cs0):* *Notifications (cs0):*
@@ -133,27 +135,48 @@ failures across all domains.
not exposed, user isolation, GET by ID shape, not found 404, forbidden 403, not exposed, user isolation, GET by ID shape, not found 404, forbidden 403,
git-credentials empty envelope, auth required (11 tests) 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 | ### v0.28.3 — ICD Close-out + Frontend Decomposition Prep
|----------|-----|----------------|-------| ICD straggler sweep (rolled in from v0.28.2 tail) and frontend
| 1 | `projects.md` | 0 | Passed clean, needs full trace | decomposition groundwork.
| 2 | `websocket.md` | — | Document event shapes only (not testable in runner) |
- [ ] Notification handler + store tests (PG + SQLite) **ICD close-out (cs0):**
- [ ] WebSocket event contract audit: document `message.created`, `workflow.advanced`, - [x] `websocket.md` full rewrite: event envelope field (`event` not `type`),
`presence.changed`, `typing`, `workflow.assigned` event shapes in ICD payload shapes for 11 event types, routing table from code, room model
- [ ] Final straggler pass: run full ICD runner, fix any remaining failures 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 **WebSocket delivery fix (cs1):**
Frontend JS decomposition and ICD runner hardening. Prepares the codebase - [x] `workflow.claimed`, `workflow.advanced`, `workflow.completed` use room-scoped
for the SDK layer by untangling the current 15-file global soup. `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 **Frontend decomposition:**
- [ ] Extract shared primitives: toast, confirm, theme, API client into importable modules - [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` - [ ] ICD runner gains test tiers: `crud`, `envelope`, `security`, `sdk`
- [ ] Runner coverage target: 100% of ICD-documented endpoints have at least one test - [ ] 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) ### v0.28.4 — Security Tier (ICD Runner Red Team)
New `security` tier in ICD test runner. Multi-user fixtures already exist. New `security` tier in ICD test runner. Multi-user fixtures already exist.

View File

@@ -130,26 +130,43 @@ func (h *WorkflowAssignmentHandler) Claim(c *gin.Context) {
return return
} }
// Emit workflow.claimed WS event // Look up channel for this assignment (needed for WS delivery + notification)
if h.hub != nil {
payload, _ := json.Marshal(map[string]any{
"assignment_id": assignmentID,
"claimed_by": userID,
})
h.hub.GetBus().Publish(events.Event{
Label: "workflow.claimed",
Room: assignmentID,
Payload: payload,
Ts: now.UnixMilli(),
})
}
// Persist notification for bell/inbox (v0.28.2)
if svc := notifications.Default(); svc != nil {
var channelID string var channelID string
_ = database.DB.QueryRowContext(c.Request.Context(), _ = database.DB.QueryRowContext(c.Request.Context(),
database.Q(`SELECT channel_id FROM workflow_assignments WHERE id = $1`), database.Q(`SELECT channel_id FROM workflow_assignments WHERE id = $1`),
assignmentID).Scan(&channelID) assignmentID).Scan(&channelID)
// Emit workflow.claimed WS event to all user participants in the channel.
// Uses SendToUser (not room-scoped Bus.Publish) because room subscriptions
// are not yet wired on the client side. See websocket.md § Room Model.
if h.hub != nil && channelID != "" {
payload, _ := json.Marshal(map[string]any{
"assignment_id": assignmentID,
"claimed_by": userID,
"channel_id": channelID,
})
evt := events.Event{
Label: "workflow.claimed",
Payload: payload,
Ts: now.UnixMilli(),
}
rows, err := database.DB.QueryContext(c.Request.Context(), database.Q(`
SELECT participant_id FROM channel_participants
WHERE channel_id = $1 AND participant_type = 'user'
`), channelID)
if err == nil {
defer rows.Close()
for rows.Next() {
var uid string
if rows.Scan(&uid) == nil {
h.hub.SendToUser(uid, evt)
}
}
}
}
// Persist notification for bell/inbox (v0.28.2)
if svc := notifications.Default(); svc != nil && channelID != "" {
notifications.NotifyWorkflowClaimed(svc, userID, assignmentID, channelID) notifications.NotifyWorkflowClaimed(svc, userID, assignmentID, channelID)
} }

View File

@@ -338,18 +338,36 @@ func (h *WorkflowInstanceHandler) readWorkflowState(ctx context.Context, channel
return return
} }
// emitWorkflowEvent pushes a workflow event to all connected clients in the channel's room. // emitWorkflowEvent pushes a workflow event to all user participants in the channel.
// Uses SendToUser (not room-scoped Bus.Publish) because room subscriptions are
// not yet wired on the client side. See websocket.md § Room Model.
func (h *WorkflowInstanceHandler) emitWorkflowEvent(label, channelID string, data map[string]any) { func (h *WorkflowInstanceHandler) emitWorkflowEvent(label, channelID string, data map[string]any) {
if h.hub == nil { if h.hub == nil {
return return
} }
payload, _ := json.Marshal(data) payload, _ := json.Marshal(data)
h.hub.GetBus().Publish(events.Event{ evt := events.Event{
Label: label, Label: label,
Room: channelID,
Payload: payload, Payload: payload,
Ts: time.Now().UnixMilli(), Ts: time.Now().UnixMilli(),
}) }
// Send to all user participants in the channel
rows, err := database.DB.Query(database.Q(`
SELECT participant_id FROM channel_participants
WHERE channel_id = $1 AND participant_type = 'user'
`), channelID)
if err != nil {
log.Printf("[ws] %s: failed to query participants for channel %s: %v", label, channelID[:min(8, len(channelID))], err)
return
}
defer rows.Close()
for rows.Next() {
var uid string
if rows.Scan(&uid) == nil {
h.hub.SendToUser(uid, evt)
}
}
} }
// triggerOnComplete checks if the workflow has an on_complete chain config // triggerOnComplete checks if the workflow has an on_complete chain config

View File

@@ -4,6 +4,9 @@
// Admin actions: user management, roles, model visibility, // Admin actions: user management, roles, model visibility,
// personas, team management, global providers. // personas, team management, global providers.
(function() {
'use strict';
// ── Admin Actions ──────────────────────────── // ── Admin Actions ────────────────────────────
function _adminScroll() { return document.getElementById('adminMain'); } function _adminScroll() { return document.getElementById('adminMain'); }
@@ -883,3 +886,36 @@ async function saveAdminExtension(id) {
UI.toast(e.message, 'error'); UI.toast(e.message, 'error');
} }
} }
// ── Exports ─────────────────────────────────
window._adminPersonaForm = _adminPersonaForm;
window._initAdminListeners = _initAdminListeners;
window.adminResetUserPassword = adminResetUserPassword;
window.bulkSetUserModelVisibility = bulkSetUserModelVisibility;
window.cycleModelVisibility = cycleModelVisibility;
window.deleteAdminExtension = deleteAdminExtension;
window.deleteAdminPersona = deleteAdminPersona;
window.deleteGroup = deleteGroup;
window.deleteTeam = deleteTeam;
window.deleteUser = deleteUser;
window.deleteUserPersona = deleteUserPersona;
window.editAdminExtension = editAdminExtension;
window.editAdminPersona = editAdminPersona;
window.ensureAdminPersonaForm = ensureAdminPersonaForm;
window.removeGroupMember = removeGroupMember;
window.removeTeamMember = removeTeamMember;
window.saveGrant = saveGrant;
window.settingsDeleteTeamPersona = settingsDeleteTeamPersona;
window.settingsRemoveTeamMember = settingsRemoveTeamMember;
window.settingsUpdateTeamMember = settingsUpdateTeamMember;
window.showApproveForm = showApproveForm;
window.toggleAdminExtension = toggleAdminExtension;
window.toggleAdminPersona = toggleAdminPersona;
window.toggleTeamActive = toggleTeamActive;
window.toggleUserActive = toggleUserActive;
window.toggleUserModelVisibility = toggleUserModelVisibility;
window.toggleUserRole = toggleUserRole;
window.updateTeamMember = updateTeamMember;
window._closeExtEditForm = _closeExtEditForm;
window.saveAdminExtension = saveAdminExtension;
})();

View File

@@ -3,6 +3,11 @@
// ========================================== // ==========================================
// v0.25.0: Surface lifecycle management. // v0.25.0: Surface lifecycle management.
// Renders in the "Surfaces" admin section via ADMIN_LOADERS. // Renders in the "Surfaces" admin section via ADMIN_LOADERS.
//
// Exports: window._loadAdminSurfaces
(function() {
'use strict';
async function _loadAdminSurfaces() { async function _loadAdminSurfaces() {
const container = document.getElementById('adminSurfacesContent'); const container = document.getElementById('adminSurfacesContent');
@@ -191,3 +196,7 @@ async function _loadSurfaceList() {
} }
} }
// ── Exports ─────────────────────────────────
window._loadAdminSurfaces = _loadAdminSurfaces;
})();

View File

@@ -7,8 +7,13 @@
// BASE_PATH: injected via index.html <script> // BASE_PATH: injected via index.html <script>
// at container startup. All API paths are // at container startup. All API paths are
// prefixed automatically. // prefixed automatically.
//
// Exports: window.API
// ========================================== // ==========================================
(function() {
'use strict';
const BASE = window.__BASE__ || ''; const BASE = window.__BASE__ || '';
const _storageKey = BASE ? `sb_auth_${BASE.replace(/\//g, '')}` : 'sb_auth'; const _storageKey = BASE ? `sb_auth_${BASE.replace(/\//g, '')}` : 'sb_auth';
@@ -987,3 +992,6 @@ const API = {
return this._parseJSON(resp, path); return this._parseJSON(resp, path);
} }
}; };
window.API = API;
})();

View File

@@ -4,6 +4,12 @@
// Loaded by base.html for ALL surfaces. Contains the App state // Loaded by base.html for ALL surfaces. Contains the App state
// object and universal data-loading functions (models, settings). // object and universal data-loading functions (models, settings).
// Surface-specific logic lives in app.js (chat), admin-scaffold.js, etc. // Surface-specific logic lives in app.js (chat), admin-scaffold.js, etc.
//
// Exports: window.App, window.fetchModels
// ==========================================
(function() {
'use strict';
const App = { const App = {
chats: [], chats: [],
@@ -130,3 +136,7 @@ async function fetchModels() {
console.log(`📋 Loaded ${App.models.length} models (${App.models.filter(m => m.isPersona).length} personas, ${App.models.filter(m => m.hidden).length} hidden)`); console.log(`📋 Loaded ${App.models.length} models (${App.models.filter(m => m.isPersona).length} personas, ${App.models.filter(m => m.hidden).length} hidden)`);
} catch (e) { console.warn('Model fetch failed:', e.message); } } catch (e) { console.warn('Model fetch failed:', e.message); }
} }
window.App = App;
window.fetchModels = fetchModels;
})();

View File

@@ -6,6 +6,12 @@
// (loaded by base.html for all surfaces). // (loaded by base.html for all surfaces).
// Domain logic lives in chat.js, notes.js, settings-handlers.js, // Domain logic lives in chat.js, notes.js, settings-handlers.js,
// admin-handlers.js. UI rendering in ui-*.js files. // admin-handlers.js. UI rendering in ui-*.js files.
//
// Exports: handleLogin, handleRegister, handleLogout, switchAuthTab,
// startApp, initBanners
(function() {
'use strict';
async function init() { async function init() {
console.log('🔀 Chat Switchboard initializing...'); console.log('🔀 Chat Switchboard initializing...');
@@ -574,3 +580,12 @@ async function initBanners() {
// ── Boot ───────────────────────────────────── // ── Boot ─────────────────────────────────────
document.addEventListener('DOMContentLoaded', init); document.addEventListener('DOMContentLoaded', init);
// ── Exports ─────────────────────────────────
window.handleLogin = handleLogin;
window.handleRegister = handleRegister;
window.handleLogout = handleLogout;
window.switchAuthTab = switchAuthTab;
window.startApp = startApp;
window.initBanners = initBanners;
})();

View File

@@ -6,6 +6,11 @@
// - @mention autocomplete in chat input // - @mention autocomplete in chat input
// - Model attribution on assistant messages // - Model attribution on assistant messages
// ========================================== // ==========================================
//
// Exports: window.ChannelModels
(function() {
'use strict';
const ChannelModels = { const ChannelModels = {
@@ -429,3 +434,7 @@ function _shortName(fullName) {
.trim() .trim()
.replace(/\s+/g, '-'); // spaces → hyphens .replace(/\s+/g, '-'); // spaces → hyphens
} }
// ── Exports ─────────────────────────────────
window.ChannelModels = ChannelModels;
})();

View File

@@ -4,6 +4,11 @@
// v0.22.7: Mountable, self-contained chat pane instances. // v0.22.7: Mountable, self-contained chat pane instances.
// Replaces the hardcoded singleton pattern. // Replaces the hardcoded singleton pattern.
// ChatPane.primary is the backward-compat bridge. // ChatPane.primary is the backward-compat bridge.
//
// Exports: window.ChatPane
(function() {
'use strict';
const ChatPane = { const ChatPane = {
...createComponentRegistry('ChatPane'), ...createComponentRegistry('ChatPane'),
@@ -145,3 +150,7 @@ const ChatPane = {
active() { return this.primary; }, active() { return this.primary; },
}; };
// ── Exports ─────────────────────────────────
window.ChatPane = ChatPane;
})();

View File

@@ -3,6 +3,16 @@
// ========================================== // ==========================================
// Chat management, send, stream, regenerate, edit, branch, // Chat management, send, stream, regenerate, edit, branch,
// summarize, per-chat model persistence. // summarize, per-chat model persistence.
//
// Exports: ChatInput, loadChats, selectChat, newChat, newGroupChat,
// deleteChat, startRenameChat, sendMessage, stopGeneration,
// reloadActivePath, regenerateMessage, regenerate, editMessage,
// submitEdit, cancelEdit, switchSibling, advanceWorkflowStage,
// rejectWorkflowStage, _initChatListeners,
// summarizeAndContinue, toggleChannelAutoCompact
(function() {
'use strict';
// ── Chat Input Abstraction ────────────────── // ── Chat Input Abstraction ──────────────────
// Wraps CM6 editor (when available) or fallback textarea. // Wraps CM6 editor (when available) or fallback textarea.
@@ -1246,3 +1256,27 @@ async function rejectWorkflowStage() {
UI.toast('Reject failed: ' + (e.message || e), 'error'); UI.toast('Reject failed: ' + (e.message || e), 'error');
} }
} }
// ── Exports ─────────────────────────────────
window.ChatInput = ChatInput;
window.loadChats = loadChats;
window.selectChat = selectChat;
window.newChat = newChat;
window.newGroupChat = newGroupChat;
window.deleteChat = deleteChat;
window.startRenameChat = startRenameChat;
window.sendMessage = sendMessage;
window.stopGeneration = stopGeneration;
window.reloadActivePath = reloadActivePath;
window.regenerateMessage = regenerateMessage;
window.regenerate = regenerate;
window.editMessage = editMessage;
window.submitEdit = submitEdit;
window.cancelEdit = cancelEdit;
window.switchSibling = switchSibling;
window.advanceWorkflowStage = advanceWorkflowStage;
window.rejectWorkflowStage = rejectWorkflowStage;
window._initChatListeners = _initChatListeners;
window.summarizeAndContinue = summarizeAndContinue;
window.toggleChannelAutoCompact = toggleChannelAutoCompact;
})();

View File

@@ -17,6 +17,11 @@
// editor.isModified('src/main.go'); // → false // editor.isModified('src/main.go'); // → false
// await editor.saveAll(); // await editor.saveAll();
// editor.destroy(); // editor.destroy();
//
// Exports: window.CodeEditor
(function() {
'use strict';
const CodeEditor = { const CodeEditor = {
...createComponentRegistry('CodeEditor'), ...createComponentRegistry('CodeEditor'),
@@ -324,3 +329,7 @@ function _ceDetectLanguage(path) {
}; };
return map[ext] || 'text'; return map[ext] || 'text';
} }
// ── Exports ─────────────────────────────────
window.CodeEditor = CodeEditor;
})();

View File

@@ -7,6 +7,11 @@
// //
// Activate: Ctrl+K → "debug" or tap the 🐛 badge (appears after init) // Activate: Ctrl+K → "debug" or tap the 🐛 badge (appears after init)
// ========================================== // ==========================================
//
// Exports: window.DebugLog,window.openDebugModal,window.switchDebugTab,window.clearDebugLog,window.copyDebugLog,window.exportDebugLog,window.runDebugDiagnostics,window.purgeCache
(function() {
'use strict';
const DebugLog = { const DebugLog = {
entries: [], entries: [],
@@ -661,3 +666,14 @@ async function purgeCache() {
// Initialize ASAP — before app.js init() so we capture everything // Initialize ASAP — before app.js init() so we capture everything
DebugLog.init(); DebugLog.init();
// ── Exports ─────────────────────────────────
window.DebugLog = DebugLog;
window.openDebugModal = openDebugModal;
window.switchDebugTab = switchDebugTab;
window.clearDebugLog = clearDebugLog;
window.copyDebugLog = copyDebugLog;
window.exportDebugLog = exportDebugLog;
window.runDebugDiagnostics = runDebugDiagnostics;
window.purgeCache = purgeCache;
})();

View File

@@ -18,6 +18,11 @@
// onMove(delta, ctx), // delta = px from start (signed) // onMove(delta, ctx), // delta = px from start (signed)
// onEnd(ctx), // cleanup / persist // onEnd(ctx), // cleanup / persist
// }); // });
//
// Exports: window.initDragResize
(function() {
'use strict';
/** /**
* Attach drag-resize behavior to a handle element. * Attach drag-resize behavior to a handle element.
@@ -92,3 +97,7 @@ function _clientPos(e, isHoriz) {
} }
return isHoriz ? e.clientX : e.clientY; return isHoriz ? e.clientX : e.clientY;
} }
// ── Exports ─────────────────────────────────
window.initDragResize = initDragResize;
})();

View File

@@ -9,8 +9,13 @@
// Events.on('chat.message.*', (payload, meta) => { ... }); // Events.on('chat.message.*', (payload, meta) => { ... });
// Events.emit('chat.typing.abc123', { user: 'jeff' }); // Events.emit('chat.typing.abc123', { user: 'jeff' });
// Events.connect('/ws'); // Events.connect('/ws');
//
// Exports: window.Events
// ========================================== // ==========================================
(function() {
'use strict';
const Events = { const Events = {
_handlers: new Map(), // label → Set<{fn, once}> _handlers: new Map(), // label → Set<{fn, once}>
@@ -191,9 +196,9 @@ const Events = {
} }
// Add auth token as query param (WebSocket doesn't support headers) // Add auth token as query param (WebSocket doesn't support headers)
const tokens = JSON.parse(localStorage.getItem(typeof _storageKey !== 'undefined' ? _storageKey : 'sb_auth') || '{}'); const token = (typeof API !== 'undefined' && API.accessToken) ? API.accessToken : null;
if (tokens.accessToken) { if (token) {
url += (url.includes('?') ? '&' : '?') + `token=${encodeURIComponent(tokens.accessToken)}`; url += (url.includes('?') ? '&' : '?') + `token=${encodeURIComponent(token)}`;
} else { } else {
console.warn('[EventBus] No auth token — skipping WebSocket'); console.warn('[EventBus] No auth token — skipping WebSocket');
return; return;
@@ -325,3 +330,6 @@ const Events = {
return subs; return subs;
} }
}; };
window.Events = Events;
})();

View File

@@ -14,6 +14,11 @@
// //
// Load order: events.js → extensions.js → [ext scripts] → api.js → app.js // Load order: events.js → extensions.js → [ext scripts] → api.js → app.js
// ========================================== // ==========================================
//
// Exports: window.Extensions
(function() {
'use strict';
const Extensions = { const Extensions = {
@@ -481,3 +486,7 @@ const Extensions = {
return { extensions: exts, rendererCount: this._renderers.length }; return { extensions: exts, rendererCount: this._renderers.length };
}, },
}; };
// ── Exports ─────────────────────────────────
window.Extensions = Extensions;
})();

View File

@@ -16,6 +16,11 @@
// await tree.refresh(); // await tree.refresh();
// tree.setActiveFile('src/main.go'); // tree.setActiveFile('src/main.go');
// tree.destroy(); // tree.destroy();
//
// Exports: window.FileTree
(function() {
'use strict';
const FileTree = { const FileTree = {
...createComponentRegistry('FileTree'), ...createComponentRegistry('FileTree'),
@@ -261,3 +266,7 @@ function _ftFileIcon(name) {
}; };
return icons[ext] || '📄'; return icons[ext] || '📄';
} }
// ── Exports ─────────────────────────────────
window.FileTree = FileTree;
})();

View File

@@ -9,6 +9,9 @@
// Consumed by: chat.js (send flow), ui-core.js (message rendering), // Consumed by: chat.js (send flow), ui-core.js (message rendering),
// ui-admin.js (storage tab) // ui-admin.js (storage tab)
(function() {
'use strict';
// ── Constants ─────────────────────────────── // ── Constants ───────────────────────────────
const FILE_POLL_INTERVAL = 2000; // ms between extraction status checks const FILE_POLL_INTERVAL = 2000; // ms between extraction status checks
@@ -834,3 +837,24 @@ function _hasDragFiles(e) {
if (!e.dataTransfer?.types) return false; if (!e.dataTransfer?.types) return false;
return e.dataTransfer.types.includes('Files'); return e.dataTransfer.types.includes('Files');
} }
// ── Exports ─────────────────────────────────
window._initFileListeners = _initFileListeners;
window._pollTimers = _pollTimers;
window._startPolling = _startPolling;
window._stopPolling = _stopPolling;
window.clearStaged = clearStaged;
window.closeLightbox = closeLightbox;
window.consumeStaged = consumeStaged;
window.getStagedFileIds = getStagedFileIds;
window.hasStagedFiles = hasStagedFiles;
window.initFiles = initFiles;
window.isSendBlocked = isSendBlocked;
window.loadAdminStorage = loadAdminStorage;
window.loadAuthImages = loadAuthImages;
window.loadChannelFiles = loadChannelFiles;
window.renderMessageFiles = renderMessageFiles;
window.unstageFile = unstageFile;
window.openLightbox = openLightbox;
window.downloadFile = downloadFile;
})();

View File

@@ -4,6 +4,11 @@
// User-facing memory management in Settings, // User-facing memory management in Settings,
// admin memory review in Admin Panel, // admin memory review in Admin Panel,
// and persona memory config in persona forms. // and persona memory config in persona forms.
//
// Exports: window.MemoryUI
(function() {
'use strict';
const MemoryUI = { const MemoryUI = {
@@ -362,3 +367,7 @@ function _debounce(fn, ms) {
timer = setTimeout(() => fn.apply(this, args), ms); timer = setTimeout(() => fn.apply(this, args), ms);
}; };
} }
// ── Exports ─────────────────────────────────
window.MemoryUI = MemoryUI;
})();

View File

@@ -11,6 +11,11 @@
// sel.select(modelId); // sel.select(modelId);
// sel.getSelected(); // → current model ID // sel.getSelected(); // → current model ID
// sel.destroy(); // sel.destroy();
//
// Exports: window.ModelSelector
(function() {
'use strict';
const ModelSelector = { const ModelSelector = {
...createComponentRegistry('ModelSelector'), ...createComponentRegistry('ModelSelector'),
@@ -167,3 +172,7 @@ const ModelSelector = {
return instance; return instance;
}, },
}; };
// ── Exports ─────────────────────────────────
window.ModelSelector = ModelSelector;
})();

View File

@@ -18,6 +18,11 @@
// The existing notes.js + PanelRegistry integration continues to // The existing notes.js + PanelRegistry integration continues to
// work on the chat surface. This component is for NEW mount points // work on the chat surface. This component is for NEW mount points
// (editor pane, future notes-studio layout). // (editor pane, future notes-studio layout).
//
// Exports: window.NoteEditor
(function() {
'use strict';
const NoteEditor = { const NoteEditor = {
...createComponentRegistry('NoteEditor'), ...createComponentRegistry('NoteEditor'),
@@ -433,3 +438,7 @@ const NoteEditor = {
}, },
}; };
// ── Exports ─────────────────────────────────
window.NoteEditor = NoteEditor;
})();

View File

@@ -5,6 +5,11 @@
// connections. Lazy-loaded — initializes only // connections. Lazy-loaded — initializes only
// when the graph view is opened. // when the graph view is opened.
// ========================================== // ==========================================
//
// Exports: window.openNoteGraph,window.closeNoteGraph,window._render,window._graphResetZoom,window._graphToggleOrphans,window.invalidateNoteGraph
(function() {
'use strict';
var _graphData = null; // cached { nodes, edges, unresolved } var _graphData = null; // cached { nodes, edges, unresolved }
var _graphState = null; // { panX, panY, zoom, animId, canvas, ctx } var _graphState = null; // { panX, panY, zoom, animId, canvas, ctx }
@@ -488,3 +493,12 @@ function invalidateNoteGraph() {
_graphDirty = true; _graphDirty = true;
_folderColorMap = {}; _folderColorMap = {};
} }
// ── Exports ─────────────────────────────────
window.openNoteGraph = openNoteGraph;
window.closeNoteGraph = closeNoteGraph;
window._render = _render;
window._graphResetZoom = _graphResetZoom;
window._graphToggleOrphans = _graphToggleOrphans;
window.invalidateNoteGraph = invalidateNoteGraph;
})();

View File

@@ -3,6 +3,9 @@
// ========================================== // ==========================================
// Notes panel: editor, multi-select, folders, CRUD. // Notes panel: editor, multi-select, folders, CRUD.
(function() {
'use strict';
var _editingNoteId = null; var _editingNoteId = null;
var _notesSelectMode = false; var _notesSelectMode = false;
var _selectedNoteIds = new Set(); var _selectedNoteIds = new Set();
@@ -940,3 +943,18 @@ function _initNotesListeners() {
// No-op: kept for backward compatibility with app.js calling sequence. // No-op: kept for backward compatibility with app.js calling sequence.
// All wiring moved into _registerNotesPanel(). // All wiring moved into _registerNotesPanel().
} }
// ── Exports ─────────────────────────────────
window._currentNote = _currentNote;
window._editingNoteId = _editingNoteId;
window._initNotesListeners = _initNotesListeners;
window._registerNotesPanel = _registerNotesPanel;
window.deleteNote = deleteNote;
window.openNoteEditor = openNoteEditor;
window.openNotes = openNotes;
window.saveMessageToNote = saveMessageToNote;
window.saveNote = saveNote;
window._toggleNoteSelect = _toggleNoteSelect;
window._navigateToLinkedNote = _navigateToLinkedNote;
window.toggleBacklinks = toggleBacklinks;
})();

View File

@@ -4,6 +4,11 @@
───────────────────────────────────────────── */ ───────────────────────────────────────────── */
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
//
// Exports: window.NotifPrefs
(function() {
'use strict';
const NotifPrefs = { const NotifPrefs = {
_loaded: false, _loaded: false,
_prefs: [], _prefs: [],
@@ -161,3 +166,7 @@ NotifPrefs._getAdminSmtp = function() {
if (pw) config.smtp_password = pw; if (pw) config.smtp_password = pw;
return config; return config;
}; };
// ── Exports ─────────────────────────────────
window.NotifPrefs = NotifPrefs;
})();

View File

@@ -6,6 +6,11 @@
// //
// Dependencies: API, Events, PanelRegistry, UI // Dependencies: API, Events, PanelRegistry, UI
// ========================================== // ==========================================
//
// Exports: window.Notifications
(function() {
'use strict';
const Notifications = { const Notifications = {
@@ -423,3 +428,7 @@ function _timeAgo(isoStr) {
if (sec < 604800) return `${Math.floor(sec / 86400)}d ago`; if (sec < 604800) return `${Math.floor(sec / 86400)}d ago`;
return d.toLocaleDateString(); return d.toLocaleDateString();
} }
// ── Exports ─────────────────────────────────
window.Notifications = Notifications;
})();

View File

@@ -9,8 +9,13 @@
// - Cascading model select (provider change → filter models) // - Cascading model select (provider change → filter models)
// - Admin save operations (roles, routing, providers, teams, users, settings) // - Admin save operations (roles, routing, providers, teams, users, settings)
// - Table filtering // - Table filtering
//
// Exports: window.Pages, window._val (used by pages-splash.js)
// ========================================== // ==========================================
(function() {
'use strict';
const Pages = { const Pages = {
// ── Model Select Cascading ─────────────── // ── Model Select Cascading ───────────────
@@ -340,3 +345,8 @@ async function _api(method, path, body) {
return false; return false;
} }
} }
// ── Exports ─────────────────────────────────
window.Pages = Pages;
window._val = _val; // used by pages-splash.js
})();

View File

@@ -18,6 +18,11 @@
// const fileTree = layout.getPane('files'); // const fileTree = layout.getPane('files');
// layout.resize('files', 250); // layout.resize('files', 250);
// layout.destroy(); // layout.destroy();
//
// Exports: window.PaneContainer
(function() {
'use strict';
const PaneContainer = { const PaneContainer = {
_presets: {}, _presets: {},
@@ -581,3 +586,7 @@ PaneContainer.registerPreset('split', {
{ type: 'leaf', id: 'secondary', component: null }, { type: 'leaf', id: 'secondary', component: null },
], ],
}); });
// ── Exports ─────────────────────────────────
window.PaneContainer = PaneContainer;
})();

View File

@@ -14,6 +14,11 @@
// .workspace-primary ← main content (chat / notes / editor) // .workspace-primary ← main content (chat / notes / editor)
// .workspace-handle ← drag-resize between panes // .workspace-handle ← drag-resize between panes
// .workspace-secondary ← panel pages (preview / notes / project) // .workspace-secondary ← panel pages (preview / notes / project)
//
// Exports: window.PanelRegistry,window.toggleSidePanelFullscreen,window._initWorkspaceResize,window._initPanelSwipe,window._initPanelResponsive,window._initPanelOverlay
(function() {
'use strict';
// ── Panel Registry ────────────────────────── // ── Panel Registry ──────────────────────────
@@ -441,3 +446,12 @@ function closeSidePanel() {
function switchSidePanelTab(tab) { function switchSidePanelTab(tab) {
PanelRegistry.open(tab); PanelRegistry.open(tab);
} }
// ── Exports ─────────────────────────────────
window.PanelRegistry = PanelRegistry;
window.toggleSidePanelFullscreen = toggleSidePanelFullscreen;
window._initWorkspaceResize = _initWorkspaceResize;
window._initPanelSwipe = _initPanelSwipe;
window._initPanelResponsive = _initPanelResponsive;
window._initPanelOverlay = _initPanelOverlay;
})();

View File

@@ -3,6 +3,11 @@
// Provides renderPersonaKBPicker(container, opts) which renders a multi-select // Provides renderPersonaKBPicker(container, opts) which renders a multi-select
// KB picker with auto_search toggles for use in persona create/edit forms. // KB picker with auto_search toggles for use in persona create/edit forms.
// Pattern: (container, options) → control object with getValues/setValues/clear. // Pattern: (container, options) → control object with getValues/setValues/clear.
//
// Exports: window.renderPersonaKBPicker,window.loadPersonaKBs,window.savePersonaKBs
(function() {
'use strict';
'use strict'; 'use strict';
@@ -175,3 +180,9 @@ async function savePersonaKBs(personaId, kbPicker, apiPrefix) {
console.warn('Failed to save persona KBs:', e.message); console.warn('Failed to save persona KBs:', e.message);
} }
} }
// ── Exports ─────────────────────────────────
window.renderPersonaKBPicker = renderPersonaKBPicker;
window.loadPersonaKBs = loadPersonaKBs;
window.savePersonaKBs = savePersonaKBs;
})();

View File

@@ -6,6 +6,9 @@
// and drag-and-drop. // and drag-and-drop.
// ========================================== // ==========================================
(function() {
'use strict';
async function loadProjects() { async function loadProjects() {
try { try {
const resp = await API.listProjects(); const resp = await API.listProjects();
@@ -2003,3 +2006,54 @@ async function _removeParticipant(channelId, recordId) {
UI.toast('Failed to remove: ' + e.message, 'error'); UI.toast('Failed to remove: ' + e.message, 'error');
} }
} }
// ── Exports ─────────────────────────────────
window._registerProjectPanel = _registerProjectPanel;
window.createProject = createProject;
window.cycleChannelAiMode = cycleChannelAiMode;
window.deleteChannel = deleteChannel;
window.deleteFolder = deleteFolder;
window.deleteProject = deleteProject;
window.loadChannels = loadChannels;
window.loadFolders = loadFolders;
window.loadProjectChannelPositions = loadProjectChannelPositions;
window.loadProjects = loadProjects;
window.loadUsers = loadUsers;
window.newChannelOrDM = newChannelOrDM;
window.newFolder = newFolder;
window.onChatDragEnd = onChatDragEnd;
window.onChatDragStart = onChatDragStart;
window.onChatSectionDrop = onChatSectionDrop;
window.onFolderDrop = onFolderDrop;
window.onProjectDragLeave = onProjectDragLeave;
window.onProjectDragOver = onProjectDragOver;
window.onProjectDrop = onProjectDrop;
window.onUnfiledDrop = onUnfiledDrop;
window.openParticipantsPanel = openParticipantsPanel;
window.openProjectPanel = openProjectPanel;
window.selectChannel = selectChannel;
window.showChannelContextMenu = showChannelContextMenu;
window.showChatContextMenu = showChatContextMenu;
window.showFolderMenu = showFolderMenu;
window.showProjectMenu = showProjectMenu;
window.toggleProjectCollapse = toggleProjectCollapse;
window.renameProject = renameProject;
window.setProjectColor = setProjectColor;
window.toggleActiveProject = toggleActiveProject;
window.moveChatToProject = moveChatToProject;
window.dismissChatContextMenu = dismissChatContextMenu;
window.createProjectAndMove = createProjectAndMove;
window.moveChatToFolder = moveChatToFolder;
window._removeProjectKB = _removeProjectKB;
window._addProjectKB = _addProjectKB;
window._removeProjectNote = _removeProjectNote;
window.showWorkspacePicker = showWorkspacePicker;
window._moveChatInProject = _moveChatInProject;
window.renameChannel = renameChannel;
window.editChannelTopic = editChannelTopic;
window.archiveChannel = archiveChannel;
window.renameFolder = renameFolder;
window._showAddParticipantSearch = _showAddParticipantSearch;
window._addParticipant = _addParticipant;
window._removeParticipant = _removeParticipant;
})();

View File

@@ -15,6 +15,11 @@
// - Multi-line: Shift+Enter for newline, Enter to run // - Multi-line: Shift+Enter for newline, Enter to run
// - Admin-gated OR ?debug=1 URL param // - Admin-gated OR ?debug=1 URL param
// ========================================== // ==========================================
//
// Exports: window.REPL
(function() {
'use strict';
const REPL = { const REPL = {
@@ -549,3 +554,7 @@ const REPL = {
// Initialized from app.js startApp() after auth is confirmed, // Initialized from app.js startApp() after auth is confirmed,
// ensuring API.user.role is available for admin gate check. // ensuring API.user.role is available for admin gate check.
// ── Exports ─────────────────────────────────
window.REPL = REPL;
})();

View File

@@ -3,6 +3,13 @@
// ========================================== // ==========================================
// Settings save, provider CRUD, avatar, command palette, // Settings save, provider CRUD, avatar, command palette,
// user model roles. // user model roles.
//
// Exports: window.loadSettings,window.saveSettings,window.updateAvatarPreview,window._userProvForm,window._userProvList,window.handleSaveAdminSettings,window._userRoleConfig,window.toggleCmdPalette,window.closeCmdPalette,window._initSettingsListeners,window._initAdminSettingsToggles
// (also: window.toggleUserModelVisibility, window.bulkSetUserModelVisibility,
// window.deleteUserPersona, window.loadTeamWorkflows — self-assigned)
(function() {
'use strict';
// ── Settings ───────────────────────────────── // ── Settings ─────────────────────────────────
@@ -892,3 +899,17 @@ window.loadTeamWorkflows = async function() {
el.innerHTML = '<div class="settings-placeholder">Failed to load workflows: ' + esc(e.message) + '</div>'; el.innerHTML = '<div class="settings-placeholder">Failed to load workflows: ' + esc(e.message) + '</div>';
} }
}; };
// ── Exports ─────────────────────────────────
window.loadSettings = loadSettings;
window.saveSettings = saveSettings;
window.updateAvatarPreview = updateAvatarPreview;
window._userProvForm = _userProvForm;
window._userProvList = _userProvList;
window.handleSaveAdminSettings = handleSaveAdminSettings;
window._userRoleConfig = _userRoleConfig;
window.toggleCmdPalette = toggleCmdPalette;
window.closeCmdPalette = closeCmdPalette;
window._initSettingsListeners = _initSettingsListeners;
window._initAdminSettingsToggles = _initAdminSettingsToggles;
})();

View File

@@ -2,6 +2,11 @@
// Chat Switchboard Token Estimation // Chat Switchboard Token Estimation
// ========================================== // ==========================================
// Context tracking, token estimation, and context warning. // Context tracking, token estimation, and context warning.
//
// Exports: window.Tokens,window.updateInputTokens,window.updateContextWarning
(function() {
'use strict';
// ── Token Estimation + Context Tracking ───── // ── Token Estimation + Context Tracking ─────
@@ -157,3 +162,9 @@ function dismissContextWarning() {
const el = document.getElementById('contextWarning'); const el = document.getElementById('contextWarning');
if (el) el.style.display = 'none'; if (el) el.style.display = 'none';
} }
// ── Exports ─────────────────────────────────
window.Tokens = Tokens;
window.updateInputTokens = updateInputTokens;
window.updateContextWarning = updateContextWarning;
})();

View File

@@ -4,6 +4,9 @@
// Fullscreen admin panel with category + section navigation. // Fullscreen admin panel with category + section navigation.
// All loadAdmin*() functions unchanged — same endpoints, same DOM IDs. // All loadAdmin*() functions unchanged — same endpoints, same DOM IDs.
(function() {
'use strict';
// ── Category → Section mapping ──────────── // ── Category → Section mapping ────────────
const ADMIN_SECTIONS = { const ADMIN_SECTIONS = {
people: ['users', 'teams', 'groups'], people: ['users', 'teams', 'groups'],
@@ -1565,3 +1568,9 @@ Object.assign(UI, {
}, },
}); });
// ── Exports ─────────────────────────────────
window.ADMIN_SECTIONS = ADMIN_SECTIONS;
window.ADMIN_LABELS = ADMIN_LABELS;
window.ADMIN_LOADERS = ADMIN_LOADERS;
})();

View File

@@ -5,6 +5,13 @@
// model selector, capability badges, user, generating state, toast, // model selector, capability badges, user, generating state, toast,
// settings modal dispatcher, export, message actions, scroll. // settings modal dispatcher, export, message actions, scroll.
// Extended by ui-settings.js and ui-admin.js via Object.assign. // Extended by ui-settings.js and ui-admin.js via Object.assign.
//
// Exports: window.UI, window.renderPersonaForm,
// window.toggleSummarizedHistory (onclick handler)
// ==========================================
(function() {
'use strict';
// ── Avatar Helper ──────────────────────────── // ── Avatar Helper ────────────────────────────
// Returns HTML for a message avatar: <img> if data URI available, emoji fallback. // Returns HTML for a message avatar: <img> if data URI available, emoji fallback.
@@ -1596,3 +1603,10 @@ const UI = {
} catch (_) { /* corrupt localStorage — ignore */ } } catch (_) { /* corrupt localStorage — ignore */ }
} }
}; };
// ── Exports ─────────────────────────────────
window.UI = UI;
window.renderPersonaForm = renderPersonaForm;
// onclick handler (called from innerHTML)
window.toggleSummarizedHistory = toggleSummarizedHistory;
})();

View File

@@ -3,6 +3,16 @@
// ========================================== // ==========================================
// Markdown rendering, code blocks, preview panel, time formatting. // Markdown rendering, code blocks, preview panel, time formatting.
// esc() is in ui-primitives.js (loaded globally in base.html). // esc() is in ui-primitives.js (loaded globally in base.html).
//
// Exports: formatMessage, clearPreview, runExtensionPostRender,
// _livePreviewUpdate, _relativeTime, _renderToolCallsHTML,
// _registerPreviewPanel,
// toggleCodeCollapse, toggleHTMLPreview, downloadCode,
// popOutExtBlock (onclick handlers)
// ==========================================
(function() {
'use strict';
// ── Message Formatting ────────────────────── // ── Message Formatting ──────────────────────
@@ -573,3 +583,19 @@ function runExtensionPostRender(container) {
Extensions.runPostRenderers(container); Extensions.runPostRenderers(container);
} }
} }
// ── Exports ─────────────────────────────────
// Cross-file function calls
window.formatMessage = formatMessage;
window.clearPreview = clearPreview;
window.runExtensionPostRender = runExtensionPostRender;
window._livePreviewUpdate = _livePreviewUpdate;
window._relativeTime = _relativeTime;
window._renderToolCallsHTML = _renderToolCallsHTML;
window._registerPreviewPanel = _registerPreviewPanel;
// onclick handlers (called from innerHTML)
window.toggleCodeCollapse = toggleCodeCollapse;
window.toggleHTMLPreview = toggleHTMLPreview;
window.downloadCode = downloadCode;
window.popOutExtBlock = popOutExtBlock;
})();

View File

@@ -4,8 +4,14 @@
// Follows the renderPersonaForm() pattern: (container, options) → control object. // Follows the renderPersonaForm() pattern: (container, options) → control object.
// Designed for extension hooks: registries are open for .add(), primitives // Designed for extension hooks: registries are open for .add(), primitives
// accept options for customization, and return handles for programmatic control. // accept options for customization, and return handles for programmatic control.
//
// Exports: esc, createComponentRegistry, componentMixin, Providers, Roles,
// renderCapBadges, renderProviderForm, renderProviderList,
// renderRoleConfig, renderUsageDashboard, showConfirm, showPrompt,
// openModal, closeModal, checkTabsOverflow, Theme, mountAvatarUpload
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
(function() {
'use strict'; 'use strict';
// ══════════════════════════════════════════════════════════════════════════════ // ══════════════════════════════════════════════════════════════════════════════
@@ -1108,3 +1114,23 @@ function mountAvatarUpload(containerEl, opts = {}) {
return { setImage: render }; return { setImage: render };
} }
// ── Exports ─────────────────────────────────
window.esc = esc;
window.createComponentRegistry = createComponentRegistry;
window.componentMixin = componentMixin;
window.Providers = Providers;
window.Roles = Roles;
window.renderCapBadges = renderCapBadges;
window.renderProviderForm = renderProviderForm;
window.renderProviderList = renderProviderList;
window.renderRoleConfig = renderRoleConfig;
window.renderUsageDashboard = renderUsageDashboard;
window.showConfirm = showConfirm;
window.showPrompt = showPrompt;
window.openModal = openModal;
window.closeModal = closeModal;
window.checkTabsOverflow = checkTabsOverflow;
window.Theme = Theme;
window.mountAvatarUpload = mountAvatarUpload;
})();

View File

@@ -3,6 +3,11 @@
// ========================================== // ==========================================
// Extends UI with settings modal tabs, team management, // Extends UI with settings modal tabs, team management,
// providers, usage, model roles, and user preferences. // providers, usage, model roles, and user preferences.
//
// Exports: none (extends window.UI via Object.assign)
(function() {
'use strict';
Object.assign(UI, { Object.assign(UI, {
// ── General Settings (Settings Surface) ── // ── General Settings (Settings Surface) ──
@@ -861,3 +866,5 @@ Object.assign(UI, {
}, },
}); });
})();

View File

@@ -10,6 +10,11 @@
// menu.setUser({ display_name: 'Jeff', username: 'jeff', avatar: null }); // menu.setUser({ display_name: 'Jeff', username: 'jeff', avatar: null });
// menu.showAdmin(true); // menu.showAdmin(true);
// menu.destroy(); // menu.destroy();
//
// Exports: window.UserMenu
(function() {
'use strict';
const UserMenu = { const UserMenu = {
...createComponentRegistry('UserMenu'), ...createComponentRegistry('UserMenu'),
@@ -131,3 +136,7 @@ const UserMenu = {
return instance; return instance;
}, },
}; };
// ── Exports ─────────────────────────────────
window.UserMenu = UserMenu;
})();