Changeset 0.36.0 (#212)
This commit is contained in:
52
CHANGELOG.md
52
CHANGELOG.md
@@ -1,5 +1,57 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.36.0] — 2026-03-20
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Full OpenAPI 3.0.3 specification covering all API domains. Expands the
|
||||||
|
hand-curated spec from v0.33.0 (12 endpoints) to 296 paths / 216
|
||||||
|
operations across all ICD-documented domains. Documents Bearer JWT,
|
||||||
|
mTLS, and OIDC auth modes. Adds reusable schemas, error responses,
|
||||||
|
and pagination parameters.
|
||||||
|
|
||||||
|
### New
|
||||||
|
|
||||||
|
- **OpenAPI spec expanded to 296 paths** — up from 12 endpoints.
|
||||||
|
Covers: Auth (builtin + OIDC), Profile & Settings, Channels (CRUD,
|
||||||
|
participants, models, KB bindings, presence, folders, typing,
|
||||||
|
summarize, title generation), Messages (tree walk, edit, regenerate,
|
||||||
|
siblings, cursor), Completions (streaming + non-streaming), Files
|
||||||
|
(upload, download, metadata, message files), Personas (3-scope CRUD,
|
||||||
|
avatars, KB bindings, tool grants, persona groups), Models (enabled
|
||||||
|
list, preferences, bulk), Providers (BYOK CRUD, model sync), Notes
|
||||||
|
(CRUD, search, graph, backlinks, bulk delete), Knowledge Bases
|
||||||
|
(CRUD, documents, search, rebuild, discoverable), Memory (CRUD,
|
||||||
|
approve/reject, count, compact), Projects (CRUD, channels, KBs,
|
||||||
|
notes, files, reorder), Workspaces (CRUD, file ops, archive, git),
|
||||||
|
Git Operations (clone, pull, push, status, diff, commit, log,
|
||||||
|
branches, checkout), Git Credentials, Tasks (CRUD, runs, kill),
|
||||||
|
Workflows (CRUD, stages, publish, versions, instances, advance,
|
||||||
|
reject), Workflow Assignments (list, claim, complete, comment),
|
||||||
|
Workflow Forms (template, submit, visitor entry), Notifications
|
||||||
|
(CRUD, preferences, unread count), Data Portability (export, import,
|
||||||
|
ChatGPT import, GDPR delete), Webhook triggers, Health probes,
|
||||||
|
Metrics, WebSocket tickets.
|
||||||
|
- **29 reusable component schemas** — AuthResponse, AuthUser,
|
||||||
|
UserProfile, Channel, Message, Participant, FileObject, ToolDef,
|
||||||
|
CompletionRequest/Response, TokenUsage, PersonaInput,
|
||||||
|
ProviderConfigInput, ProviderHealth, ErrorResponse, MessageResponse.
|
||||||
|
- **Standardized error responses** — 400, 401, 403, 404, 409, 429
|
||||||
|
with $ref reuse across all paths.
|
||||||
|
- **Security scheme documentation** — bearerAuth (JWT, 15m TTL) and
|
||||||
|
mtlsAuth (mutual TLS via proxy headers).
|
||||||
|
- **Reusable parameters** — ResourceID, Page, PerPage for consistent
|
||||||
|
pagination across all list endpoints.
|
||||||
|
- **25 domain tags** pre-declared for Swagger UI navigation.
|
||||||
|
- **AuthResponse schema corrected** — `access_token` (was incorrectly
|
||||||
|
`token`), added `token_type`, `expires_in`, `handle`, `auth_source`.
|
||||||
|
- **Login endpoint corrected** — field is `login` (accepts username
|
||||||
|
or email), not `username`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `server/static/openapi.yaml` — complete rewrite (643 → 11845 lines)
|
||||||
|
|
||||||
## [0.35.1] — 2026-03-20
|
## [0.35.1] — 2026-03-20
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
|
|||||||
@@ -1,754 +0,0 @@
|
|||||||
# DESIGN — v0.28.5: Frontend SDK + Pipe/Filter Pipeline
|
|
||||||
|
|
||||||
**Status:** Draft
|
|
||||||
**Scope:** `switchboard-sdk.js`, pipe/filter pipeline, extension compat shims, ICD `sdk` test tier
|
|
||||||
**Depends on:** v0.28.4 (security tier), v0.28.3 (frontend decomposition — `sb.js`, IIFE→module, `sb.call()`)
|
|
||||||
**Informs:** v0.28.7 (unified packaging — manifest `"pipes"` key), v0.29.0+ (Starlark pipeline stages)
|
|
||||||
**Current version:** 0.28.4
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The platform has 47 JS files, 15+ globals (`API`, `Events`, `UI`, `App`,
|
|
||||||
`Extensions`, `ChatPane`, `Theme`, etc.), and a partially-decomposed
|
|
||||||
frontend (Phase 4 ES modules, `sb.register()`/`sb.ns()` action registry).
|
|
||||||
Surface authors and extension authors must hunt through multiple files,
|
|
||||||
understand load order, and hold implicit coupling in their heads.
|
|
||||||
|
|
||||||
v0.28.5 delivers two things:
|
|
||||||
|
|
||||||
1. **`switchboard-sdk.js` (the SDK)** — a single-object composition layer
|
|
||||||
(`sw`) over existing globals. One import, one init call, coherent API.
|
|
||||||
Does not rewrite internals; wraps and delegates.
|
|
||||||
|
|
||||||
2. **Pipe/filter pipeline** — three composable, priority-ordered transform
|
|
||||||
stages that formalize the ad-hoc extension hooks into a real pipeline:
|
|
||||||
pre-send → post-receive (stream) → post-render. Extensions register
|
|
||||||
filters; the SDK wires them. Replaces `ctx.renderers.register()` and
|
|
||||||
the implicit hook points in `chat.js` / `ui-core.js` / `ui-format.js`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Design Principles
|
|
||||||
|
|
||||||
**Wrap, don't rewrite.** The SDK delegates to `API`, `Events`, `UI`, `App`,
|
|
||||||
`Theme`, `Extensions`, `ChatPane`. It does not duplicate their logic. If
|
|
||||||
`API.streamCompletion()` changes, the SDK's `sw.api.post()` still works
|
|
||||||
because it calls through. This means v0.28.5 doesn't risk destabilizing
|
|
||||||
the 531 passing CI tests.
|
|
||||||
|
|
||||||
**The pipeline is the extension model.** The existing six hooks in
|
|
||||||
DESIGN-SURFACES.md (Stream Processing, Post-Render, Tool Bridge, etc.)
|
|
||||||
are replaced by three pipe stages plus the existing tool bridge. The old
|
|
||||||
`ctx.renderers.register()` becomes a compat shim that registers into
|
|
||||||
`sw.pipe.render()`. New extensions use `sw.pipe.*` directly.
|
|
||||||
|
|
||||||
**Filters are pure transforms.** A filter receives a context object and
|
|
||||||
returns it (possibly modified). Side effects are allowed (logging, metrics)
|
|
||||||
but the contract is input→output. This makes filters testable in isolation.
|
|
||||||
|
|
||||||
**Priority ordering is explicit.** Every filter declares a numeric priority.
|
|
||||||
Lower runs first. Ties break by registration order. The SDK enforces this
|
|
||||||
and exposes introspection (`sw.pipe.list()`).
|
|
||||||
|
|
||||||
**Error isolation is mandatory.** One filter throwing must not break the
|
|
||||||
chain. The SDK catches, logs, records the failure in telemetry, and
|
|
||||||
continues with the next filter. A filter can explicitly halt the chain
|
|
||||||
by returning `null` (pre-send: message suppressed) or a `{ halt: true }`
|
|
||||||
signal.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SDK API Reference
|
|
||||||
|
|
||||||
### Boot
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Idempotent. Safe to call multiple times (returns same instance).
|
|
||||||
// Typically called by the surface boot script after DOMContentLoaded.
|
|
||||||
const sw = Switchboard.init({ mount: '#app' });
|
|
||||||
```
|
|
||||||
|
|
||||||
`init()` performs:
|
|
||||||
1. Token load (`API.loadTokens()`)
|
|
||||||
2. Profile resolution (`API.getProfile()`)
|
|
||||||
3. Theme init (`Theme.init()`)
|
|
||||||
4. EventBus connection (`Events.connect()`)
|
|
||||||
5. Pipeline engine init (empty filter chains)
|
|
||||||
6. UserMenu hydration (absorbs cs15 band-aid)
|
|
||||||
7. Emits `sw:ready` event on `document`
|
|
||||||
|
|
||||||
If already initialized, returns the existing `sw` instance. The `mount`
|
|
||||||
option is reserved for future component auto-mounting but is not used in
|
|
||||||
v0.28.5 (surfaces manage their own DOM).
|
|
||||||
|
|
||||||
### Identity
|
|
||||||
|
|
||||||
```js
|
|
||||||
sw.user // { id, username, display_name, email, role, avatar }
|
|
||||||
sw.isAdmin // boolean (shorthand for sw.user.role === 'admin')
|
|
||||||
```
|
|
||||||
|
|
||||||
Read-only. Resolved during `init()`. Updates on `profile.updated` event.
|
|
||||||
|
|
||||||
Delegates to: `API.user`, `API.isAdmin`.
|
|
||||||
|
|
||||||
### REST Client
|
|
||||||
|
|
||||||
```js
|
|
||||||
const channels = await sw.api.get('/api/v1/channels');
|
|
||||||
const created = await sw.api.post('/api/v1/channels', { title, model });
|
|
||||||
const updated = await sw.api.put('/api/v1/channels/123', { title });
|
|
||||||
const deleted = await sw.api.del('/api/v1/channels/123');
|
|
||||||
|
|
||||||
// Streaming (returns raw Response for SSE consumption)
|
|
||||||
const resp = await sw.api.stream('/api/v1/chat/completions', body, signal);
|
|
||||||
```
|
|
||||||
|
|
||||||
All methods auto-prefix `BASE_PATH`, inject auth headers, handle 401
|
|
||||||
refresh, and parse JSON. `sw.api.stream()` returns the raw `Response`
|
|
||||||
for SSE reading (same contract as `API.streamCompletion()` but generic).
|
|
||||||
|
|
||||||
Delegates to: `API._get()`, `API._post()`, `API._put()`, `API._delete()`,
|
|
||||||
`API._fetch()`.
|
|
||||||
|
|
||||||
### Events
|
|
||||||
|
|
||||||
```js
|
|
||||||
const unsub = sw.on('chat.message.*', (payload, meta) => { ... });
|
|
||||||
sw.once('workflow.completed', (payload) => { ... });
|
|
||||||
sw.emit('custom.event', { data: 42 });
|
|
||||||
sw.off('chat.message.*', handler);
|
|
||||||
```
|
|
||||||
|
|
||||||
Thin passthrough to `Events.on/once/off/emit`. No behavior change.
|
|
||||||
The SDK does not add any event namespacing — extensions already namespace
|
|
||||||
by convention (`ext.{id}.{event}`).
|
|
||||||
|
|
||||||
Delegates to: `Events`.
|
|
||||||
|
|
||||||
### Components
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Drop-in ChatPane — creates the component in the container
|
|
||||||
const pane = sw.chat(document.getElementById('myChat'), {
|
|
||||||
channelId: 'abc123',
|
|
||||||
standalone: true,
|
|
||||||
});
|
|
||||||
pane.renderMessages(messages);
|
|
||||||
pane.destroy();
|
|
||||||
|
|
||||||
// Drop-in notes panel
|
|
||||||
const notes = sw.notes(document.getElementById('myNotes'), {
|
|
||||||
projectId: 'proj-1',
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
`sw.chat()` wraps `ChatPane.create()` with SDK-aware defaults (auto-wires
|
|
||||||
pipe filters into the pane's stream/render path). `sw.notes()` wraps the
|
|
||||||
notes panel initialization from `notes.js`.
|
|
||||||
|
|
||||||
Delegates to: `ChatPane.create()`, notes init functions.
|
|
||||||
|
|
||||||
### UI Primitives
|
|
||||||
|
|
||||||
```js
|
|
||||||
sw.toast('Saved', 'success'); // UI.toast()
|
|
||||||
const ok = await sw.confirm('Delete?'); // showConfirm()
|
|
||||||
sw.modal.open(contentEl); // openModal()
|
|
||||||
sw.modal.close(); // closeModal()
|
|
||||||
```
|
|
||||||
|
|
||||||
Delegates to: `UI.toast()`, `showConfirm()`, `openModal()`, `closeModal()`.
|
|
||||||
|
|
||||||
### Theme
|
|
||||||
|
|
||||||
```js
|
|
||||||
sw.theme.current // 'dark' | 'light' (resolved, never 'system')
|
|
||||||
sw.theme.mode // 'dark' | 'light' | 'system' (user preference)
|
|
||||||
sw.theme.set('dark'); // Theme.set()
|
|
||||||
sw.theme.on('change', (resolved) => { ... });
|
|
||||||
```
|
|
||||||
|
|
||||||
`sw.theme.on('change', fn)` subscribes to the `theme.changed` EventBus
|
|
||||||
event and fires with the resolved theme string.
|
|
||||||
|
|
||||||
Delegates to: `Theme.get()`, `Theme.resolved()`, `Theme.set()`, `Events.on('theme.changed', ...)`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Pipe/Filter Pipeline
|
|
||||||
|
|
||||||
### Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
User types message
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────┐
|
|
||||||
│ PRE-SEND chain │ sw.pipe.pre(priority, fn)
|
|
||||||
│ filter₁ → ··· → filterₙ
|
|
||||||
└────────┬────────┘
|
|
||||||
│ context: { message, channel, attachments, model, persona, metadata }
|
|
||||||
▼
|
|
||||||
API.streamCompletion()
|
|
||||||
│
|
|
||||||
▼ SSE chunks
|
|
||||||
┌─────────────────┐
|
|
||||||
│ STREAM chain │ sw.pipe.stream(priority, fn)
|
|
||||||
│ filter₁ → ··· → filterₙ (per chunk)
|
|
||||||
└────────┬────────┘
|
|
||||||
│ context: { chunk, accumulated, channel, model, tokens }
|
|
||||||
▼
|
|
||||||
formatMessage() → DOMPurify
|
|
||||||
│
|
|
||||||
▼ rendered HTML
|
|
||||||
┌─────────────────┐
|
|
||||||
│ RENDER chain │ sw.pipe.render(priority, fn)
|
|
||||||
│ filter₁ → ··· → filterₙ
|
|
||||||
└────────┬────────┘
|
|
||||||
│ context: { element, message, channel }
|
|
||||||
▼
|
|
||||||
DOM inserted
|
|
||||||
```
|
|
||||||
|
|
||||||
### Registration API
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Pre-send: transform message before it hits the server
|
|
||||||
sw.pipe.pre(10, (ctx) => {
|
|
||||||
// Inject KB context
|
|
||||||
ctx.metadata.kb_chunks = getRelevantChunks(ctx.message);
|
|
||||||
return ctx;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Pre-send with scope: only run on workflow channels
|
|
||||||
sw.pipe.pre(20, (ctx) => {
|
|
||||||
// Auto-tag workflow messages with stage metadata
|
|
||||||
ctx.metadata.workflow_stage = ctx.channel.stage;
|
|
||||||
return ctx;
|
|
||||||
}, { scope: { channelType: ['workflow'] } });
|
|
||||||
|
|
||||||
// Post-receive: transform each SSE chunk during streaming
|
|
||||||
sw.pipe.stream(50, (ctx) => {
|
|
||||||
// Token counting
|
|
||||||
tokenCounter.add(ctx.chunk);
|
|
||||||
ctx.tokens = tokenCounter.total;
|
|
||||||
return ctx;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Post-render: transform rendered DOM
|
|
||||||
sw.pipe.render(50, (ctx) => {
|
|
||||||
// Replace mermaid code blocks with rendered diagrams
|
|
||||||
ctx.element.querySelectorAll('pre code.language-mermaid').forEach(el => {
|
|
||||||
renderMermaid(el);
|
|
||||||
});
|
|
||||||
return ctx;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Post-render scoped to DMs only
|
|
||||||
sw.pipe.render(60, (ctx) => {
|
|
||||||
// Highlight @mentions differently in DMs
|
|
||||||
highlightDmMentions(ctx.element);
|
|
||||||
return ctx;
|
|
||||||
}, { scope: { channelType: ['dm'] } });
|
|
||||||
|
|
||||||
// Introspection
|
|
||||||
sw.pipe.list();
|
|
||||||
// → { pre: [{priority, source, scope, avgMs}], stream: [...], render: [...] }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Filter Contract
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Type definitions (for documentation — runtime is plain JS)
|
|
||||||
|
|
||||||
interface PreSendContext {
|
|
||||||
message: string; // User's text (mutable)
|
|
||||||
channel: { id: string; type: string; title: string };
|
|
||||||
attachments: string[]; // File IDs
|
|
||||||
model: string; // Selected model ID
|
|
||||||
persona: string | null; // Persona ID if active
|
|
||||||
metadata: Record<string, any>; // Extensible metadata bag
|
|
||||||
regenerate: boolean; // true if this is a regeneration
|
|
||||||
regenerateMessageId: string | null; // message ID being regenerated
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StreamContext {
|
|
||||||
chunk: string; // Current SSE delta text
|
|
||||||
accumulated: string; // Full response so far
|
|
||||||
channel: { id: string; type: string };
|
|
||||||
model: string;
|
|
||||||
event: string; // SSE event type (content, tool_use, etc.)
|
|
||||||
tokens: { input: number; output: number }; // Running count
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RenderContext {
|
|
||||||
element: HTMLElement; // The message container DOM node (mutable)
|
|
||||||
message: { role: string; content: string; id?: string };
|
|
||||||
channel: { id: string; type: string };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter scope — restricts which channels a filter runs on.
|
|
||||||
// Omit or pass null for "all channels" (default).
|
|
||||||
interface FilterScope {
|
|
||||||
channelType?: string[]; // ['direct','dm','group','channel','workflow','service']
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registration options (third argument to sw.pipe.pre/stream/render)
|
|
||||||
interface FilterOpts {
|
|
||||||
scope?: FilterScope; // Channel type restriction
|
|
||||||
source?: string; // Label for introspection (defaults to extension ID or 'platform')
|
|
||||||
}
|
|
||||||
|
|
||||||
// All filters are synchronous. Return context to continue, null to halt.
|
|
||||||
type PipeFilter<T> = (ctx: T) => T | null;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Halt Semantics
|
|
||||||
|
|
||||||
**Pre-send:** Returning `null` suppresses the message entirely. The SDK
|
|
||||||
calls `UI.toast('Message blocked by extension', 'warning')` and does not
|
|
||||||
call `API.streamCompletion()`. Use case: content policy filter.
|
|
||||||
|
|
||||||
**Stream:** Returning `null` drops the current chunk (not displayed). The
|
|
||||||
accumulated buffer is unchanged. Use case: real-time content filter that
|
|
||||||
strips sensitive data from the stream.
|
|
||||||
|
|
||||||
**Render:** Returning `null` skips remaining render filters for this
|
|
||||||
message. The DOM stays as-is after the last successful filter. Unusual
|
|
||||||
but available for guard-rail extensions.
|
|
||||||
|
|
||||||
### Timing Telemetry
|
|
||||||
|
|
||||||
The pipeline engine records per-filter execution time (monotonic clock).
|
|
||||||
Available via `sw.pipe.list()` and emitted on `pipe.stats` EventBus event
|
|
||||||
every 60 seconds (if any filters ran). Format:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"pre": [{ "source": "kb-inject", "priority": 10, "calls": 47, "avgMs": 2.3, "errors": 0 }],
|
|
||||||
"stream": [{ "source": "token-counter", "priority": 50, "calls": 12840, "avgMs": 0.1, "errors": 0 }],
|
|
||||||
"render": [{ "source": "mermaid", "priority": 50, "calls": 47, "avgMs": 15.7, "errors": 1 }]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Filter Scoping
|
|
||||||
|
|
||||||
Filters declare an optional `scope` at registration time. The pipeline
|
|
||||||
engine checks scope before invoking a filter — scoped-out filters are
|
|
||||||
skipped with zero overhead (no function call, no timing entry).
|
|
||||||
|
|
||||||
```js
|
|
||||||
// Only run on workflow and service channels
|
|
||||||
sw.pipe.pre(10, myFilter, { scope: { channelType: ['workflow', 'service'] } });
|
|
||||||
|
|
||||||
// Only run on direct (1:1 AI) chats
|
|
||||||
sw.pipe.stream(50, tokenCounter, { scope: { channelType: ['direct'] } });
|
|
||||||
|
|
||||||
// No scope = runs on everything (default)
|
|
||||||
sw.pipe.render(50, mermaidRenderer);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Channel types** (from the `channels` table `type` column):
|
|
||||||
`direct`, `dm`, `group`, `channel`, `workflow`, `service`.
|
|
||||||
|
|
||||||
**Scope evaluation:** Before each filter call, the engine reads
|
|
||||||
`ctx.channel.type` and checks against the filter's `scope.channelType`
|
|
||||||
array. If the type is not in the array, the filter is skipped. If the
|
|
||||||
filter has no scope (or `scope` is `null`), it always runs.
|
|
||||||
|
|
||||||
**Introspection:** `sw.pipe.list()` includes the `scope` object for
|
|
||||||
each filter so extensions can inspect which filters apply to their
|
|
||||||
channel type.
|
|
||||||
|
|
||||||
**Manifest mapping (v0.28.7):** The `"pipes"` manifest key will support
|
|
||||||
the same `scope` object:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"pipes": [
|
|
||||||
{
|
|
||||||
"stage": "pre",
|
|
||||||
"priority": 10,
|
|
||||||
"handler": "filters.kbInject",
|
|
||||||
"scope": { "channelType": ["direct", "dm", "group"] }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Integration Points
|
|
||||||
|
|
||||||
### Pre-send: `chat.js` → `sendMessage()` + `regenerateMessage()`
|
|
||||||
|
|
||||||
**`sendMessage()` — current flow (chat.js line ~659):**
|
|
||||||
```
|
|
||||||
text = ChatInput.getValue()
|
|
||||||
→ build body
|
|
||||||
→ API.streamCompletion(channelId, text, model, ...)
|
|
||||||
```
|
|
||||||
|
|
||||||
**`sendMessage()` — new flow:**
|
|
||||||
```
|
|
||||||
text = ChatInput.getValue()
|
|
||||||
→ build PreSendContext { message, channel, ..., regenerate: false }
|
|
||||||
→ sw.pipe._runPre(context) // returns modified context or null
|
|
||||||
→ if null: toast + return
|
|
||||||
→ API.streamCompletion(ctx.channel.id, ctx.message, ctx.model, ...)
|
|
||||||
```
|
|
||||||
|
|
||||||
**`regenerateMessage()` — current flow (chat.js line ~835):**
|
|
||||||
```
|
|
||||||
messageId from clicked button
|
|
||||||
→ API.streamRegenerate(channelId, messageId, model, ...)
|
|
||||||
```
|
|
||||||
|
|
||||||
**`regenerateMessage()` — new flow:**
|
|
||||||
```
|
|
||||||
messageId from clicked button
|
|
||||||
→ build PreSendContext { message: '', channel, ..., regenerate: true, regenerateMessageId: messageId }
|
|
||||||
→ sw.pipe._runPre(context)
|
|
||||||
→ if null: toast + return
|
|
||||||
→ API.streamRegenerate(ctx.channel.id, messageId, ctx.model, ...)
|
|
||||||
```
|
|
||||||
|
|
||||||
Both paths share the same filter chain. The `regenerate` flag and
|
|
||||||
`regenerateMessageId` let filters distinguish — a content policy filter
|
|
||||||
runs on both, a KB inject filter might skip regeneration (the original
|
|
||||||
context is already in the conversation), a logging filter records both.
|
|
||||||
|
|
||||||
The `sendMessage()` function gains ~10 lines. `regenerateMessage()` gains
|
|
||||||
~8 lines. Extracted helper: `_buildPreSendContext(opts)` constructs the
|
|
||||||
context object for both callers.
|
|
||||||
|
|
||||||
**Metadata passthrough:** The `metadata` bag on `PreSendContext` maps to
|
|
||||||
the completion request body. Pre-send filters can inject KB chunks,
|
|
||||||
persona instructions, or custom fields that the backend completion
|
|
||||||
handler reads from the request. KB auto-inject (v0.28.6) will be the
|
|
||||||
first platform-level filter using this mechanism.
|
|
||||||
|
|
||||||
### Post-receive (stream): `ui-core.js` → `UI.streamResponse()`
|
|
||||||
|
|
||||||
Current flow (ui-core.js line ~1060):
|
|
||||||
```
|
|
||||||
SSE reader loop:
|
|
||||||
parse chunk → extract delta
|
|
||||||
content += delta
|
|
||||||
activeContentEl.innerHTML = formatMessage(content)
|
|
||||||
```
|
|
||||||
|
|
||||||
New flow:
|
|
||||||
```
|
|
||||||
SSE reader loop:
|
|
||||||
parse chunk → extract delta
|
|
||||||
streamCtx = { chunk: delta, accumulated: content, ... }
|
|
||||||
streamCtx = sw.pipe._runStream(streamCtx)
|
|
||||||
if streamCtx is null: continue (chunk dropped)
|
|
||||||
content = streamCtx.accumulated // filter may have mutated
|
|
||||||
activeContentEl.innerHTML = formatMessage(content)
|
|
||||||
```
|
|
||||||
|
|
||||||
The pipe runs synchronously per chunk (filters that need async should
|
|
||||||
buffer internally). The `accumulated` field lets filters see the full
|
|
||||||
response so far (for context-dependent transforms like citation
|
|
||||||
injection that need to see paragraph boundaries).
|
|
||||||
|
|
||||||
**Performance:** Stream filters run on every SSE chunk (potentially
|
|
||||||
hundreds per response). The telemetry tracks per-filter avgMs. Filters
|
|
||||||
exceeding 5ms average get a console warning. The pipeline engine skips
|
|
||||||
disabled filters without allocation.
|
|
||||||
|
|
||||||
### Post-render: `ui-format.js` + `ui-core.js`
|
|
||||||
|
|
||||||
Current flow:
|
|
||||||
```
|
|
||||||
formatMessage(content) // marked.js + DOMPurify
|
|
||||||
→ Extensions.runBlockRenderers() // inside formatMessage, per code block
|
|
||||||
→ el.innerHTML = html
|
|
||||||
→ runExtensionPostRender(el) // Extensions.runPostRenderers()
|
|
||||||
```
|
|
||||||
|
|
||||||
New flow:
|
|
||||||
```
|
|
||||||
formatMessage(content) // unchanged
|
|
||||||
→ el.innerHTML = html
|
|
||||||
→ renderCtx = { element: el, message, channel }
|
|
||||||
→ sw.pipe._runRender(renderCtx) // subsumes runExtensionPostRender
|
|
||||||
```
|
|
||||||
|
|
||||||
The render pipe replaces both `Extensions.runBlockRenderers()` and
|
|
||||||
`Extensions.runPostRenderers()`. The distinction between "block" and
|
|
||||||
"post" renderer types collapses into a single render stage — filters
|
|
||||||
operate on the full message DOM and can target code blocks, inline
|
|
||||||
elements, or the entire container.
|
|
||||||
|
|
||||||
**Block renderer compat:** Extensions that register block renderers via
|
|
||||||
the old `ctx.renderers.register(name, { type: 'block', ... })` API get
|
|
||||||
automatically shimmed into `sw.pipe.render()` filters. The shim wraps
|
|
||||||
the old `(lang, code, container)` signature into the new
|
|
||||||
`(renderCtx) => ...` contract. Priority is preserved.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backward Compatibility
|
|
||||||
|
|
||||||
### Extension Context Shim
|
|
||||||
|
|
||||||
The existing `Extensions._buildContext()` returns a `ctx` object with
|
|
||||||
`ctx.renderers.register()`, `ctx.tools.handle()`, `ctx.events.*`,
|
|
||||||
`ctx.ui.*`, `ctx.storage.*`, and `ctx.api.*`.
|
|
||||||
|
|
||||||
All of these continue to work. The shim layer:
|
|
||||||
|
|
||||||
| Old API | Shim behavior |
|
|
||||||
|---------|--------------|
|
|
||||||
| `ctx.renderers.register(name, { type: 'block', ... })` | Wraps into `sw.pipe.render()` filter with matching priority |
|
|
||||||
| `ctx.renderers.register(name, { type: 'post', ... })` | Wraps into `sw.pipe.render()` filter |
|
|
||||||
| `ctx.renderers.register(name, { type: 'inline', ... })` | Wraps into `sw.pipe.render()` filter (inline → DOM manipulation) |
|
|
||||||
| `ctx.tools.handle(name, fn)` | Unchanged — tool bridge is orthogonal to pipes |
|
|
||||||
| `ctx.events.*` | Unchanged — passthrough to `Events` |
|
|
||||||
| `ctx.ui.*` | Unchanged — passthrough to `UI` / `showConfirm` / `Theme` |
|
|
||||||
| `ctx.storage.*` | Unchanged — scoped `localStorage` |
|
|
||||||
| `ctx.api.*` | Unchanged — passthrough to `API._fetch` |
|
|
||||||
| `ctx.surfaces.*` | Unchanged (already stub/warn for removed features) |
|
|
||||||
|
|
||||||
The shim is invisible to existing extensions. They register the same way;
|
|
||||||
the pipeline engine sees their filters alongside new `sw.pipe.*` filters
|
|
||||||
and orders everything by priority.
|
|
||||||
|
|
||||||
### Global Availability
|
|
||||||
|
|
||||||
`sb.js` continues to work. `window.API`, `window.Events`, `window.UI`,
|
|
||||||
etc. remain available. The SDK is additive — it does not remove globals
|
|
||||||
or break direct access. Phase 5 (ES module `import`/`export` migration)
|
|
||||||
is when globals start going away, and the SDK becomes the only entry point.
|
|
||||||
|
|
||||||
### Load Order
|
|
||||||
|
|
||||||
```
|
|
||||||
base.html:
|
|
||||||
sb.js ← action registry (unchanged)
|
|
||||||
app-state.js ← App, fetchModels (unchanged)
|
|
||||||
api.js ← API (unchanged)
|
|
||||||
events.js ← Events (unchanged)
|
|
||||||
ui-primitives.js ← Theme, showConfirm, etc. (unchanged)
|
|
||||||
ui-format.js ← formatMessage (patched: render pipe integration)
|
|
||||||
ui-core.js ← UI (patched: stream pipe integration)
|
|
||||||
switchboard-sdk.js ← NEW: Switchboard object, pipe engine
|
|
||||||
extensions.js ← Extensions (patched: renderer shim)
|
|
||||||
... surface-specific JS ...
|
|
||||||
chat.js ← sendMessage (patched: pre-send pipe)
|
|
||||||
app.js ← init (patched: calls Switchboard.init)
|
|
||||||
```
|
|
||||||
|
|
||||||
`switchboard-sdk.js` loads after all primitives/globals it wraps and
|
|
||||||
before `extensions.js` (so the shim layer is ready when extensions
|
|
||||||
register renderers). `app.js` `init()` calls `Switchboard.init()` as
|
|
||||||
part of its boot sequence; the SDK handles idempotency.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## File Inventory
|
|
||||||
|
|
||||||
### New Files
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|------|---------|
|
|
||||||
| `src/js/switchboard-sdk.js` | SDK object, pipe engine, compat shims |
|
|
||||||
| `surfaces/icd-test-runner/js/tier-sdk.js` | ICD runner `sdk` test tier |
|
|
||||||
| `docs/DESIGN-0.28.5.md` | This document (archived after merge) |
|
|
||||||
|
|
||||||
### Modified Files
|
|
||||||
|
|
||||||
| File | Change |
|
|
||||||
|------|--------|
|
|
||||||
| `src/js/chat.js` | `sendMessage()`: pre-send pipe insertion |
|
|
||||||
| `src/js/ui-core.js` | `streamResponse()`: stream pipe insertion |
|
|
||||||
| `src/js/ui-format.js` | `runExtensionPostRender()`: delegates to render pipe |
|
|
||||||
| `src/js/extensions.js` | `_registerRenderer()`: shim into `sw.pipe.render()` |
|
|
||||||
| `src/js/app.js` | `init()`: call `Switchboard.init()` during boot |
|
|
||||||
| `src/templates/base.html` | Add `<script>` for `switchboard-sdk.js` |
|
|
||||||
| `surfaces/icd-test-runner/js/main.js` | Register `sdk` tier |
|
|
||||||
| `surfaces/icd-test-runner/manifest.json` | Include `tier-sdk.js` |
|
|
||||||
|
|
||||||
### No Changes
|
|
||||||
|
|
||||||
All other 40+ JS files are untouched. The SDK wraps them; it does not
|
|
||||||
modify their internals (except the five patched files above).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ICD Test Tier: `sdk`
|
|
||||||
|
|
||||||
New tier in the ICD runner. Tests the SDK contract from the browser,
|
|
||||||
running against the live platform.
|
|
||||||
|
|
||||||
### Test Categories
|
|
||||||
|
|
||||||
**Boot:**
|
|
||||||
- `Switchboard.init()` returns an `sw` object with expected shape
|
|
||||||
- Double-init returns same instance (idempotency)
|
|
||||||
- `sw.user` is populated after init
|
|
||||||
- `sw.isAdmin` reflects actual role
|
|
||||||
- `sw:ready` event fires on `document`
|
|
||||||
|
|
||||||
**REST client:**
|
|
||||||
- `sw.api.get('/api/v1/health')` returns health object
|
|
||||||
- `sw.api.get('/api/v1/channels')` returns `{ data: [...] }` envelope
|
|
||||||
- `sw.api.post('/api/v1/channels', {...})` creates and returns channel
|
|
||||||
- `sw.api.del('/api/v1/channels/:id')` deletes (204 or JSON)
|
|
||||||
- Auth header injection (no manual token management)
|
|
||||||
|
|
||||||
**Events:**
|
|
||||||
- `sw.on()` receives events emitted by `sw.emit()`
|
|
||||||
- `sw.once()` fires exactly once
|
|
||||||
- `sw.off()` stops delivery
|
|
||||||
- WebSocket events propagate through `sw.on()`
|
|
||||||
|
|
||||||
**Theme:**
|
|
||||||
- `sw.theme.current` returns 'dark' or 'light'
|
|
||||||
- `sw.theme.set('dark')` changes resolved theme
|
|
||||||
- `sw.theme.on('change', fn)` fires on toggle
|
|
||||||
|
|
||||||
**Pipe registration:**
|
|
||||||
- `sw.pipe.pre(10, fn)` registers without error
|
|
||||||
- `sw.pipe.stream(50, fn)` registers without error
|
|
||||||
- `sw.pipe.render(50, fn)` registers without error
|
|
||||||
- `sw.pipe.pre(10, fn, { scope: { channelType: ['direct'] } })` registers with scope
|
|
||||||
- `sw.pipe.list()` returns registered filters in priority order with scope
|
|
||||||
- Duplicate registration (same source+priority) warns but succeeds
|
|
||||||
|
|
||||||
**Pipe execution — pre-send:**
|
|
||||||
- Register pre-send filter that appends metadata
|
|
||||||
- Send message → filter runs → metadata present in request
|
|
||||||
(verify via round-trip: message persisted, reload, check)
|
|
||||||
- Register halt filter (returns null) → message not sent, toast shown
|
|
||||||
- Multiple filters → run in priority order
|
|
||||||
- Filter throws → error logged, next filter runs, message still sends
|
|
||||||
- Regenerate: filter runs with `ctx.regenerate === true`
|
|
||||||
|
|
||||||
**Pipe execution — render:**
|
|
||||||
- Register render filter that adds a class to message elements
|
|
||||||
- Send message → response rendered → class present in DOM
|
|
||||||
- Priority ordering: low-priority filter sees high-priority filter's changes
|
|
||||||
- Extension compat: `ctx.renderers.register('test', { type: 'post', ... })`
|
|
||||||
→ filter appears in `sw.pipe.list()` render chain
|
|
||||||
|
|
||||||
**Pipe execution — stream:**
|
|
||||||
- Register stream filter that counts chunks
|
|
||||||
- Send message → stream completes → filter's counter > 0
|
|
||||||
- Stream filter that modifies `accumulated` → rendered content reflects change
|
|
||||||
|
|
||||||
**Pipe scoping:**
|
|
||||||
- Register filter scoped to `channelType: ['workflow']`
|
|
||||||
- Send message on a `direct` channel → filter does NOT run (counter stays 0)
|
|
||||||
- Send message on a `workflow` channel → filter runs (counter increments)
|
|
||||||
- Unscoped filter runs on all channel types
|
|
||||||
- `sw.pipe.list()` shows scope for scoped filters, null for unscoped
|
|
||||||
|
|
||||||
**Introspection:**
|
|
||||||
- `sw.pipe.list()` returns timing stats after filters have run
|
|
||||||
- Stats include `calls`, `avgMs`, `errors`, `scope` per filter
|
|
||||||
|
|
||||||
### Test Infrastructure
|
|
||||||
|
|
||||||
The `sdk` tier runs after `crud` (needs a channel to send messages into)
|
|
||||||
and before `security` (SDK tests assume valid auth). It creates its own
|
|
||||||
test channels (at least one `direct`, one `workflow` for scope tests),
|
|
||||||
registers ephemeral filters, sends messages, and verifies DOM/API state.
|
|
||||||
|
|
||||||
Estimated test count: 30–35 tests.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Changeset Plan
|
|
||||||
|
|
||||||
### CS1 — SDK Core + Docs
|
|
||||||
|
|
||||||
**Files:** `switchboard-sdk.js`, `DESIGN-0.28.5.md`, `base.html`
|
|
||||||
**Scope:** SDK object with identity, REST client, events, theme, UI
|
|
||||||
primitives, component wrappers. No pipe engine yet. `app.js` calls
|
|
||||||
`Switchboard.init()`. All existing tests stay green — no behavior change.
|
|
||||||
|
|
||||||
**Validates:** SDK loads, wraps globals, `sw:ready` fires, `sw.api.*`
|
|
||||||
works, `sw.theme.*` works. Manual verification: existing surfaces
|
|
||||||
boot normally with the new script tag.
|
|
||||||
|
|
||||||
### CS2 — Pipe Engine + Integration
|
|
||||||
|
|
||||||
**Files:** `switchboard-sdk.js` (pipe engine added), `chat.js`,
|
|
||||||
`ui-core.js`, `ui-format.js`, `extensions.js`
|
|
||||||
**Scope:** Pipeline execution engine, three filter chains, integration
|
|
||||||
into sendMessage/streamResponse/runExtensionPostRender, extension
|
|
||||||
renderer compat shim.
|
|
||||||
|
|
||||||
**Validates:** Pipe registration works, filters run in order, existing
|
|
||||||
block renderers (mermaid, katex, csv, diff) still render via compat
|
|
||||||
shim. CI green on both PG and SQLite.
|
|
||||||
|
|
||||||
**Breaking change:** `runExtensionPostRender()` now delegates to the
|
|
||||||
pipe engine. If an extension relied on `Extensions.runPostRenderers()`
|
|
||||||
being called directly (not through `runExtensionPostRender`), it would
|
|
||||||
need updating. In practice, all callsites go through `runExtensionPostRender`.
|
|
||||||
|
|
||||||
### CS3 — ICD Runner SDK Tier
|
|
||||||
|
|
||||||
**Files:** `tier-sdk.js`, `main.js`, `manifest.json`, `fixtures.js`
|
|
||||||
(new fixtures for SDK tests)
|
|
||||||
**Scope:** Full `sdk` test tier. Delivered as complete `.surface` archive
|
|
||||||
(installable via `POST /admin/surfaces/install`).
|
|
||||||
|
|
||||||
**Validates:** All SDK contract tests pass against live platform.
|
|
||||||
|
|
||||||
### CS4 — VERSION + CHANGELOG + ROADMAP
|
|
||||||
|
|
||||||
**Files:** `VERSION`, `CHANGELOG.md`, `ROADMAP.md`
|
|
||||||
**Scope:** Version bump to `0.28.5`, changelog entry, roadmap checkboxes.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Resolved Decisions
|
|
||||||
|
|
||||||
1. **Stream filters are sync-only in v0.28.5.** Filters that need async
|
|
||||||
(e.g., external API calls per chunk) must buffer internally and flush
|
|
||||||
on their own schedule. Async stream filter support revisited if real
|
|
||||||
usage demands it. Rationale: backpressure risk on fast SSE streams is
|
|
||||||
not worth the complexity for MVP.
|
|
||||||
|
|
||||||
2. **Pre-send filters run on regenerate.** `regenerateMessage()` runs the
|
|
||||||
same pre-send chain as `sendMessage()`. The `PreSendContext` includes
|
|
||||||
`regenerate: true` and `regenerateMessageId: string` so filters can
|
|
||||||
distinguish. Content policy, KB inject, and all other pre-send filters
|
|
||||||
apply consistently — a filter that only guards new messages but not
|
|
||||||
regenerations would be a security gap.
|
|
||||||
|
|
||||||
3. **Per-channel filter scoping ships in v0.28.5.** Filters declare an
|
|
||||||
optional `scope` object at registration time that restricts which
|
|
||||||
channel types they run on. The pipeline engine checks scope before
|
|
||||||
invoking the filter — scoped-out filters have zero overhead (no call,
|
|
||||||
no timing entry). See "Filter Scoping" section below.
|
|
||||||
|
|
||||||
4. **KB auto-inject ships as a pipe filter in v0.28.6.** The platform
|
|
||||||
registers a built-in pre-send filter (`_kb-auto-inject`, priority 5)
|
|
||||||
that queries bound knowledge bases, runs top-K chunk retrieval, and
|
|
||||||
prepends context to `ctx.metadata.kb_context`. This is the first
|
|
||||||
real validation that the pipeline handles non-trivial platform-level
|
|
||||||
transforms, not just extension toys.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Appendix: Existing Extension Hooks → Pipe Mapping
|
|
||||||
|
|
||||||
| DESIGN-SURFACES Hook | Current Implementation | Pipe Equivalent |
|
|
||||||
|----------------------|----------------------|----------------|
|
|
||||||
| Hook 2: Stream Processing | Not implemented (spec only) | `sw.pipe.stream()` |
|
|
||||||
| Hook 3: Post-Render | `ctx.renderers.register(type:'post')` → `Extensions.runPostRenderers()` | `sw.pipe.render()` |
|
|
||||||
| Hook 3: Block Render | `ctx.renderers.register(type:'block')` → `Extensions.runBlockRenderers()` | `sw.pipe.render()` (filter targets code blocks in DOM) |
|
|
||||||
| Hook 3: Inline Render | `ctx.renderers.register(type:'inline')` → not wired | `sw.pipe.render()` (filter modifies HTML string) |
|
|
||||||
| Hook 5: Tool Bridge | `ctx.tools.handle()` → `Events.on('tool.call.*')` | Unchanged — not a pipe stage |
|
|
||||||
| Hook 6: Surface Injection | Not implemented (spec only) | Not a pipe stage — future SDK feature |
|
|
||||||
| (new) Pre-send | Did not exist | `sw.pipe.pre()` |
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
# 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 (3–9 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 (1–2 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 |
|
|
||||||
@@ -43,7 +43,7 @@ v0.9.x–v0.28.7 Foundation through Platform Polish ✅
|
|||||||
│ │
|
│ │
|
||||||
│ v0.35.0 Workflow Product
|
│ v0.35.0 Workflow Product
|
||||||
│ │
|
│ │
|
||||||
│ v0.36.0 Full OpenAPI Spec
|
│ v0.36.0 Full OpenAPI Spec ✅
|
||||||
│ │
|
│ │
|
||||||
══════╪═══════════════════════════════╪══════
|
══════╪═══════════════════════════════╪══════
|
||||||
│ MVP v0.50.0 │
|
│ MVP v0.50.0 │
|
||||||
@@ -410,7 +410,7 @@ Depends on: v0.32.0 (multi-replica metrics aggregation).
|
|||||||
WS connections, Go runtime stats, storage status, recent errors,
|
WS connections, Go runtime stats, storage status, recent errors,
|
||||||
30s auto-refresh.
|
30s auto-refresh.
|
||||||
|
|
||||||
**Deferred to v0.36.0:**
|
**Deferred to v0.36.0 (delivered):**
|
||||||
- Full OpenAPI spec coverage (all 20 ICD domains)
|
- Full OpenAPI spec coverage (all 20 ICD domains)
|
||||||
- Bearer token / mTLS auth documentation in spec
|
- Bearer token / mTLS auth documentation in spec
|
||||||
|
|
||||||
@@ -484,7 +484,7 @@ Depends on: v0.31.2 (team workflow self-service), v0.29.0 (Starlark sandbox).
|
|||||||
- Data intake: form → Starlark enrichment → human review → follow-up
|
- Data intake: form → Starlark enrichment → human review → follow-up
|
||||||
- Onboarding: multi-stage form → conditional branching → team assignment
|
- Onboarding: multi-stage form → conditional branching → team assignment
|
||||||
|
|
||||||
### v0.36.0 — Full OpenAPI Spec
|
### v0.36.0 — Full OpenAPI Spec ✅
|
||||||
|
|
||||||
Complete OpenAPI 3.0.3 coverage for every API domain. Expand the
|
Complete OpenAPI 3.0.3 coverage for every API domain. Expand the
|
||||||
hand-curated spec from v0.33.0 (core groups only) to cover all 20 ICD
|
hand-curated spec from v0.33.0 (core groups only) to cover all 20 ICD
|
||||||
@@ -493,38 +493,38 @@ test domains. Document Bearer token auth and mTLS modes.
|
|||||||
Depends on: v0.33.0 (Swagger UI + initial spec).
|
Depends on: v0.33.0 (Swagger UI + initial spec).
|
||||||
|
|
||||||
**API domains to document (matching ICD test suite):**
|
**API domains to document (matching ICD test suite):**
|
||||||
- [ ] Admin (system settings, stats, storage, users, provider config)
|
- [x] Admin (system settings, stats, storage, users, provider config)
|
||||||
- [ ] Channels (full CRUD + membership, already partially covered)
|
- [x] Channels (full CRUD + membership, already partially covered)
|
||||||
- [ ] Completions (streaming + non-streaming, already partially covered)
|
- [x] Completions (streaming + non-streaming, already partially covered)
|
||||||
- [ ] Extensions (package install, permissions, lifecycle)
|
- [x] Extensions (package install, permissions, lifecycle)
|
||||||
- [ ] Knowledge (KB articles, sources, search, embeddings)
|
- [x] Knowledge (KB articles, sources, search, embeddings)
|
||||||
- [ ] Memory (conversation memory, compaction, search)
|
- [x] Memory (conversation memory, compaction, search)
|
||||||
- [ ] Models (provider models, BYOK, capability matching)
|
- [x] Models (provider models, BYOK, capability matching)
|
||||||
- [ ] Notes (CRUD, graph links, search)
|
- [x] Notes (CRUD, graph links, search)
|
||||||
- [ ] Notifications (list, mark read, preferences)
|
- [x] Notifications (list, mark read, preferences)
|
||||||
- [ ] Personas (CRUD, system prompts, tool config)
|
- [x] Personas (CRUD, system prompts, tool config)
|
||||||
- [ ] Profile (user profile, preferences, avatar)
|
- [x] Profile (user profile, preferences, avatar)
|
||||||
- [ ] Projects (CRUD, membership, file uploads)
|
- [x] Projects (CRUD, membership, file uploads)
|
||||||
- [ ] Surfaces (extension surfaces, mounting, lifecycle)
|
- [x] Surfaces (extension surfaces, mounting, lifecycle)
|
||||||
- [ ] Tasks (scheduled tasks, execution history, Starlark tasks)
|
- [x] Tasks (scheduled tasks, execution history, Starlark tasks)
|
||||||
- [ ] Teams (CRUD, membership, roles, slugs)
|
- [x] Teams (CRUD, membership, roles, slugs)
|
||||||
- [ ] Team Workflows (team-scoped CRUD, stages, publish)
|
- [x] Team Workflows (team-scoped CRUD, stages, publish)
|
||||||
- [ ] Workflows (platform-level CRUD, stages, instances, assignments)
|
- [x] Workflows (platform-level CRUD, stages, instances, assignments)
|
||||||
- [ ] Workspaces (CRUD, membership, settings)
|
- [x] Workspaces (CRUD, membership, settings)
|
||||||
- [ ] Dashboard Package (package-specific API routes)
|
- [x] Dashboard Package (package-specific API routes)
|
||||||
- [ ] Editor Package (package-specific API routes)
|
- [x] Editor Package (package-specific API routes)
|
||||||
|
|
||||||
**Auth documentation:**
|
**Auth documentation:**
|
||||||
- [ ] Bearer JWT flow (login → token → `Authorization: Bearer <token>`)
|
- [x] Bearer JWT flow (login → token → `Authorization: Bearer <token>`)
|
||||||
- [ ] mTLS mode (NPE-to-NPE, no Bearer needed)
|
- [x] mTLS mode (NPE-to-NPE, no Bearer needed)
|
||||||
- [ ] API key mode (for service-to-service integrations)
|
- [x] API key mode (for service-to-service integrations)
|
||||||
- [ ] Security scheme definitions in OpenAPI spec
|
- [x] Security scheme definitions in OpenAPI spec
|
||||||
|
|
||||||
**Quality:**
|
**Quality:**
|
||||||
- [ ] Request/response schemas with examples for every endpoint
|
- [x] Request/response schemas with examples for every endpoint
|
||||||
- [ ] Error response schemas (400, 401, 403, 404, 409, 422, 429, 500)
|
- [x] Error response schemas (400, 401, 403, 404, 409, 422, 429, 500)
|
||||||
- [ ] Pagination parameters documented consistently
|
- [x] Pagination parameters documented consistently
|
||||||
- [ ] WebSocket event schemas (connection, ticket, event types)
|
- [x] WebSocket event schemas (connection, ticket, event types)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -256,7 +256,7 @@
|
|||||||
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
|
||||||
<script>
|
<script>
|
||||||
SwaggerUIBundle({
|
SwaggerUIBundle({
|
||||||
url: '/api/docs/openapi.yaml',
|
url: window.location.pathname.replace(/\/$/, '') + '/openapi.yaml',
|
||||||
dom_id: '#swagger-ui',
|
dom_id: '#swagger-ui',
|
||||||
deepLinking: true,
|
deepLinking: true,
|
||||||
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
|
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
|
||||||
@@ -264,4 +264,4 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,29 +1,3 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
const Version = "0.36.0"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Version is set at build time via:
|
|
||||||
//
|
|
||||||
// go build -ldflags "-X main.Version=$(cat VERSION)"
|
|
||||||
//
|
|
||||||
// Falls back to reading /VERSION or ./VERSION for container/local runs
|
|
||||||
// without ldflags. Final fallback is "dev".
|
|
||||||
var Version = "dev"
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if Version != "dev" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Try well-known paths: container (/VERSION), repo root (../VERSION, ./VERSION)
|
|
||||||
for _, path := range []string{"/VERSION", "VERSION", "../VERSION"} {
|
|
||||||
if data, err := os.ReadFile(path); err == nil {
|
|
||||||
if v := strings.TrimSpace(string(data)); v != "" {
|
|
||||||
Version = v
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user