Files
waggle-os/docs/backend-map/sections/04-feature-map.md
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

526 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 04 — Frontend Feature → API Map
**Purpose.** This section is the contract a Lovable rebuild must reproduce. It enumerates every OS app, overlay, and page in the Waggle web client (`apps/web/src/`), the single HTTP/SSE/WS client (`adapter`) they all share, the global providers/state, and — for each feature — the exact backend endpoints it calls. Everything below is grounded in the actual code; identifiers, paths, and field names are quoted verbatim from source.
---
## 1. Mental Model
Waggle's web UI is a **single-page "desktop OS"**, not a multi-page app. React Router (`apps/web/src/App.tsx`) defines only two routes:
| Path | Element | File |
|---|---|---|
| `/` | `<Index />` | `apps/web/src/pages/Index.tsx` |
| `*` | `<NotFound />` | `apps/web/src/pages/NotFound.tsx` |
`Index.tsx` renders a `BootScreen` (gated by `localStorage["waggle-booted"]`) and then `<Desktop />` (`apps/web/src/components/os/Desktop.tsx`). **`Desktop.tsx` is the real shell**: it owns the window manager, the dock, all app windows, and all overlays. There is no per-app routing — apps are opened as draggable windows by `appId`.
```mermaid
flowchart TD
App["App.tsx (BrowserRouter)"] --> Index["pages/Index.tsx"]
Index --> Boot["BootScreen"]
Index --> Desktop["components/os/Desktop.tsx<br/>(the OS shell)"]
Desktop --> WM["useWindowManager<br/>(open/close/focus windows)"]
Desktop --> Dock["Dock (dock-tiers.ts config)"]
Desktop --> AppWindows["AppWindow x N<br/>renderAppContent(win) switch on appId"]
Desktop --> Overlays["Overlays (modals/rails)"]
AppWindows --> Apps["*App.tsx components"]
Apps --> Hooks["hooks/use*.ts"]
Apps --> Adapter
Hooks --> Adapter["lib/adapter.ts<br/>(LocalAdapter singleton)"]
Adapter --> Backend["Fastify sidecar @ http://127.0.0.1:3333"]
```
Two things every Lovable rebuild MUST recreate first:
1. **The `adapter` singleton** (`apps/web/src/lib/adapter.ts`) — one `LocalAdapter` instance exported as `export const adapter = new LocalAdapter()`. Every component and hook imports this same instance. It is the only thing that talks to the backend.
2. **`ServiceProvider`** (`apps/web/src/providers/ServiceProvider.tsx`) — the only React context provider. It calls `adapter.connect()` once on mount and exposes `{ adapter, connected, connecting, error, reconnect }` via `useService()`.
---
## 2. API Client Contract (`lib/adapter.ts`)
### 2.1 Base URL & connection
| Concern | Behavior (from code) |
|---|---|
| Default server | `const DEFAULT_SERVER = 'http://127.0.0.1:3333'` |
| Base URL resolution | constructor: `serverUrl ?? localStorage.getItem('waggle:server-url') ?? DEFAULT_SERVER` |
| Change server | `adapter.setServerUrl(url)` — persists to `localStorage["waggle:server-url"]`, resets connected flags |
| Connect | `adapter.connect()``healthProbe()` (GET `/health`) then `fetchSessionToken()`; sets `_connected = true` |
| Auto-rediscovery | `healthProbe()` falls back to `DEFAULT_SERVER` once if the stored URL fails, and persists the working URL |
| Connection getters | `adapter.isConnected`, `adapter.hasAttemptedConnect`, `adapter.getServerUrl()` |
### 2.2 Auth / header pattern
Auth is a **bearer token fetched from a same-origin bootstrap**, not a login form:
- On `connect()`, `fetchSessionToken()` does GET `/api/auth/session-token``{ token }`, stored in `this.authToken`.
- Every request goes through `adapter.fetch(path, init)` which:
- Adds `Content-Type: application/json` **only when a body is present** and no content-type was supplied (a deliberate fix — bodyless POSTs must not send JSON content-type).
- Adds `Authorization: Bearer <token>` when `authToken` is set.
- On HTTP **403** with body `{ error: 'TIER_INSUFFICIENT' }`, dispatches a global `window` event `waggle:tier-insufficient` with `{ required, actual, message }` (this drives the `UpgradeModal`).
- All requests use `fetchWithTimeout` (`lib/fetch-utils.ts`, default 10s; uploads/ingest use 30s) which throws `TimeoutError` / `NetworkError`.
### 2.3 Streaming patterns
| Pattern | Method(s) | Transport |
|---|---|---|
| Chat token stream | `async *sendMessage(...)` | `POST /api/chat` returning an SSE-formatted body, parsed manually (`event:` / `data:` lines) into `StreamEvent` |
| Server-Sent Events (named/default) | `private subscribeSSE(path, onData)` | `EventSource`; used by `subscribeEvents`, `subscribeNotifications`, `subscribeWaggleDance` |
| Named SSE event | `subscribeSubagentStatus(...)` | `EventSource.addEventListener('subagent_status', ...)` on `/api/notifications/stream` |
| Harvest progress | `subscribeHarvestProgress(...)` | `EventSource` on `/api/harvest/progress`; returns `{ ready: Promise, close }` |
| WebSocket | `connectWebSocket(onMessage)` | `new WebSocket(baseUrl→ws + /ws?token=<authToken>)` |
The chat SSE event names are normalized inside `sendMessage`: `token→token`, `tool→tool_start`, `tool_result→tool_end`, `done`, `error`, `step`, `approval_request`. `useChat` additionally handles `approval_required` and `model_switch`.
### 2.4 Response normalization helpers (rebuild must mirror these)
The backend and the frontend contract disagree on several field names; the adapter normalizes on read. A Lovable rebuild that talks to the same backend must reproduce these mappings or it will crash on `undefined`:
| Helper | Maps |
|---|---|
| `unwrapArray<T>(data)` | accepts raw array OR `{ results: [...] }` / `{ key: [...] }` envelopes |
| `normalizeFrame(raw)` | `frameType` codes `I/F/E/D/T/N``insight/fact/event/decision/task/entity`; `importance` string `low/normal/high/critical` ↔ number `1-4` |
| `normalizeCronJob(raw)` | server `cronExpr/lastRunAt/nextRunAt` → client `schedule/lastRun/nextRun` |
| `getFleet()` | server `durationMs/tokensUsed` → client `duration/tokenUsage` |
| `getModelPricing()` | server `inputPer1k/outputPer1k` → client `inputCostPer1k/outputCostPer1k` |
| `getMemoryStats()` | server `frameCount/entityCount/relationCount` → client `frames/entities/relations`; tolerates `workspace: null` |
| `getModel()` | accepts raw string OR `{ model }` |
### 2.5 Tauri dual-path
Several memory methods branch on `isTauri()` (`lib/tauri-bindings.ts`) and use Rust IPC instead of HTTP when running inside the desktop binary: `addMemoryFrame`, `searchMemory`, `getKnowledgeGraph`, `getIdentity`. A web-only Lovable rebuild uses the HTTP path exclusively (the `else` branch of each).
---
## 3. Complete Endpoint Reference
Every adapter method below maps to a backend route. Method = HTTP verb the adapter issues. "Stream" = SSE/WS. Request/response shapes are the adapter's declared TS types (verbatim).
### 3.1 Auth / Health / System
| Method | Path | Request | Response | Stream |
|---|---|---|---|---|
| GET | `/health` | — | `SystemHealth { status, uptime, services[] }` | no |
| GET | `/api/auth/session-token` | — | `{ token? }` | no |
| GET | `/api/agent/status` | — | `AgentStatus { model, tokensUsed, costUsd, isActive }` | no |
| GET | `/api/agent/cost` | — | `{ totalCost, totalTokens }` | no |
| GET | `/api/agent/model` | — | `string \| { model }` | no |
| PUT | `/api/agent/model` | `{ model }` | — | no |
| POST | `/api/agent/abort` | `{ workspaceId }` | — | no |
### 3.2 Workspaces & Templates
| Method | Path | Request | Response |
|---|---|---|---|
| GET | `/api/workspaces` | — | `Workspace[]` (normalizes `personaId``persona`) |
| POST | `/api/workspaces` | `{ name, group, persona?/personaId?, agentGroupId?, templateId?, shared?, model? }` | `Workspace` |
| PUT | `/api/workspaces/:id` | `Partial<Workspace>` | `Workspace` |
| PATCH | `/api/workspaces/:id` | `Partial<{persona, agentGroupId, templateId, name, group, model}>` | `Workspace` |
| DELETE | `/api/workspaces/:id` | — | — |
| GET | `/api/workspaces/:id/context` | — | `WorkspaceContext` |
| GET | `/api/workspaces/:id/files` | — | `unknown[]` |
| GET | `/api/workspace-templates` | — | `{ templates: WorkspaceTemplate[], count }` |
| POST | `/api/workspace-templates` | `Omit<WorkspaceTemplate,'id'\|'builtIn'>` | `WorkspaceTemplate` |
| POST | `/api/workspace-templates/generate` | `{ prompt, availableConnectors[], availableCommands[], availablePersonas[] }` | template |
| PUT | `/api/workspace-templates/:id` | template | `WorkspaceTemplate` |
| DELETE | `/api/workspace-templates/:id` | — | — |
| GET | `/api/browse/local?path=` | — | `{ entries[{name,path,type}], current }` |
| POST | `/api/browse/local/mkdir` | `{ path }` | `{ name, path, type }` |
### 3.3 Files
| Method | Path | Request | Response |
|---|---|---|---|
| GET | `/api/workspaces/:id/files/list?path=` | — | `FileEntry[]` |
| POST | `/api/workspaces/:id/files/upload` | `FormData(file, path)` (30s timeout) | `FileEntry` |
| GET | `/api/workspaces/:id/files/download?path=` | — | `Blob` |
| POST | `/api/workspaces/:id/files/mkdir` | `{ path }` | `FileEntry` |
| POST | `/api/workspaces/:id/files/delete` | `{ path }` | — |
| POST | `/api/workspaces/:id/files/move` | `{ from, to }` | `FileEntry` |
| POST | `/api/workspaces/:id/files/copy` | `{ from, to }` | `FileEntry` |
| GET | `/api/workspaces/:id/documents` | — | `{ documents[{name, versions[]}] }` |
| GET | `/api/workspaces/:id/documents/:name/versions` | — | `{ versions[] }` |
### 3.4 Chat / Sessions / Pins / History / Feedback
| Method | Path | Request | Response | Stream |
|---|---|---|---|---|
| POST | `/api/chat` | `{ workspaceId, message, sessionId?, persona?, autonomy?, shape }` | SSE body of `StreamEvent`s | **yes (SSE)** |
| DELETE | `/api/chat/history?session=` | — | — | no |
| GET | `/api/history?workspace=&session=` | — | `ChatMessage[]` | no |
| GET | `/api/workspaces/:id/sessions` | — | `Session[]` | no |
| POST | `/api/workspaces/:id/sessions` | — | `Session` | no |
| PATCH | `/api/sessions/:id?workspace=` | `{ title }` | — | no |
| DELETE | `/api/sessions/:id?workspace=` | — | — | no |
| GET | `/api/workspaces/:id/sessions/search?q=` | — | `Session[]` | no |
| GET | `/api/workspaces/:id/sessions/:sid/export` | — | `string` | no |
| GET | `/api/workspaces/:id/pins` | — | `{ pins[] }` | no |
| POST | `/api/workspaces/:id/pins` | `{ messageContent, messageRole, label? }` | pin | no |
| DELETE | `/api/workspaces/:id/pins/:pinId` | — | — | no |
| POST | `/api/feedback` | `{ sessionId, messageIndex, rating, reason?, detail? }` | — (fire-and-forget) | no |
### 3.5 Memory / Knowledge Graph / Identity
| Method | Path | Request | Response |
|---|---|---|---|
| GET | `/api/memory/frames?limit=&workspace=` | — | `MemoryFrame[]` (normalized) |
| POST | `/api/memory/frames` | `Omit<MemoryFrame,'id'>` | `MemoryFrame` |
| PUT | `/api/memory/frames/:id` | `Partial<MemoryFrame>` | `MemoryFrame` |
| DELETE | `/api/memory/frames/:id` | — | — |
| PATCH | `/api/memory/frames/:id/access?workspace=` | — | `{ accessCount }` |
| GET | `/api/memory/search?q=&scope=` | — | `MemoryFrame[]` |
| GET | `/api/memory/graph?workspace=` / `?scope=all\|personal` | — | `{ nodes: KGNode[], edges: KGEdge[] }` |
| GET | `/api/memory/stats` | — | `{ personal, workspace, total }` each `{frames,entities,relations}` |
| GET | `/api/identity` | — | `IdentityResponse { configured, name, ... }` |
| GET | `/api/team/memory/search?q=&limit=` | — | `{ results[] }` |
| GET | `/api/mind/identity` · `/api/mind/awareness` · `/api/mind/skills` | — | `unknown` |
### 3.6 Local Inference (Ollama)
| Method | Path | Response |
|---|---|---|
| GET | `/api/local-inference/hardware` | `{ hardware, source }` |
| GET | `/api/local-inference/models?useCase=` | `{ models[], source }` |
| GET | `/api/local-inference/status` | `{ servers[], ollamaInstalled, totalLocalModels }` |
| POST | `/api/local-inference/pull` | `{ ok }` (body `{ model }`) |
### 3.7 Events / Timeline / Stats
| Method | Path | Response | Stream |
|---|---|---|---|
| GET | `/api/events?workspaceId=` | `AgentStep[]` | no |
| GET | `/api/events?workspaceId=&limit=&from=` | `TimelineEvent[]` | no |
| GET | `/api/events/stats` | `{ byType, total, dailyBreakdown? }` | no |
| — | `/api/events/stream` | `AgentStep` | **yes (SSE)** |
### 3.8 Skills / Capabilities / Marketplace
| Method | Path | Request | Response |
|---|---|---|---|
| GET | `/api/skills` | — | `SkillPack[]` (forced `installed:true`) |
| POST | `/api/skills/create` | `{ name, description }` | — |
| GET | `/api/skills/starter-pack/catalog` | — | `SkillPack[]` (maps `family`→category) |
| POST | `/api/skills/starter-pack/:skillId` | `{}` | — (throws with status/body on !ok) |
| GET | `/api/skills/capability-packs/catalog` | — | `SkillPack[]` |
| GET | `/api/skills/test` | — | (raw `adapter.fetch`, CapabilitiesApp) |
| GET | `/api/capabilities/status` | — | `unknown` |
| GET | `/api/marketplace/packs` | — | `SkillPack[]` |
| GET | `/api/marketplace/search?query=&limit=` | — | raw `Response` |
| GET | `/api/marketplace/installed` | — | raw `Response` |
| POST | `/api/marketplace/install` | `{ packageId }` | raw `Response` |
| POST | `/api/marketplace/uninstall` | `{ packageId }` | raw `Response` |
> Note: marketplace search/installed/install/uninstall return the raw `Response` so callers can do status-aware handling (403 → `UpgradeModal`). All four go through authenticated `adapter.fetch` (a raw `fetch` 401s before the session token bootstraps).
### 3.9 Fleet / Agents / Agent Groups / Jobs
| Method | Path | Request | Response |
|---|---|---|---|
| GET | `/api/fleet` | — | `FleetSession[]` (normalized) |
| POST | `/api/fleet/:workspaceId/(pause\|resume\|kill)` | — | — (`stop``kill`) |
| POST | `/api/fleet/spawn` | `{ task, persona?, model?, parentWorkspaceId? }` | `FleetSession` (throws on !ok) |
| GET | `/api/personas` | — | `Persona[]` |
| POST | `/api/personas` | `{ name, description, icon?, systemPrompt, tools? }` | `Persona` |
| PATCH | `/api/personas/:id` | persona patch | `unknown` |
| DELETE | `/api/personas/:id` | — | — |
| POST | `/api/personas/generate` | `{ prompt }` | `{ name, description, systemPrompt, tools[] }` |
| GET | `/api/agent-groups` | — | `unknown[]` |
| POST | `/api/agent-groups` | `{ name, description, strategy, members[] }` | `unknown` |
| PATCH | `/api/agent-groups/:id` | group patch | `unknown` |
| DELETE | `/api/agent-groups/:id` | — | — |
| POST | `/api/agent-groups/:id/run` | `{ task, teamId:'default' }` | `unknown` |
| GET | `/api/jobs/:jobId` | — | `{ status, startedAt?, completedAt?, output? } \| null` |
| POST | `/api/jobs/:jobId/cancel` | — | — |
### 3.10 Cron / Scheduled Jobs
| Method | Path | Request | Response |
|---|---|---|---|
| GET | `/api/cron` | — | `CronJob[]` (normalized) |
| POST | `/api/cron` | `{ name, cronExpr, jobType, jobConfig?, workspaceId?, enabled? }` | `CronJob` |
| PUT | `/api/cron/:id` | `Partial<CronJob>` | `CronJob` |
| DELETE | `/api/cron/:id` | — | — |
| POST | `/api/cron/:id/trigger` | — | `{ triggered, autoEnabled?, schedule? }` |
### 3.11 Notifications / Approvals
| Method | Path | Request | Response | Stream |
|---|---|---|---|---|
| — | `/api/notifications/stream` | — | `Notification` / `subagent_status` event | **yes (SSE)** |
| GET | `/api/notifications/history` | — | `Notification[]` | no |
| PATCH | `/api/notifications/:id/read` | — | — | no |
| POST | `/api/notifications/read-all` | — | — | no |
| GET | `/api/approval/pending` | — | `{ pending[{requestId,toolName,input,timestamp}], count }` | no |
| POST | `/api/approval/:requestId` | `{ approved, always, sourceWorkspaceId }` | — | no |
| GET | `/api/approval/grants` | — | `{ grants[], count }` | no |
| DELETE | `/api/approval/grants/:id` | — | — | no |
| POST | `/api/approval/grants/clear` | — | — | no |
### 3.12 Settings / Permissions / Providers / LiteLLM
| Method | Path | Request | Response |
|---|---|---|---|
| GET | `/api/settings` | — | `Settings` |
| PUT | `/api/settings` | `Partial<Settings>` | — |
| GET | `/api/settings/permissions` | — | `{ defaultAutonomy, externalGates[], workspaceOverrides }` |
| PUT | `/api/settings/permissions` | partial | — |
| POST | `/api/settings/test-key` | `{ provider, apiKey }` | `{ valid }` |
| GET | `/api/providers` | — | `{ providers[], search[], activeSearch }` |
| GET | `/api/litellm/models` | — | `string[]` |
| GET | `/api/litellm/status` | — | `unknown` |
| GET | `/api/litellm/pricing` | — | `ModelPricing[]` (normalized) |
| GET | `/api/debug/logs` | — | (raw, SettingsApp) |
| GET | `/api/export` · POST `/api/backup` · POST `/api/restore` | — | (raw, SettingsApp/BackupApp) |
### 3.13 Connectors / Vault / Profile
| Method | Path | Request | Response |
|---|---|---|---|
| GET | `/api/connectors` | — | `Connector[]` |
| GET | `/api/connectors/:id/health` | — | `unknown` |
| POST | `/api/connectors/:id/connect` | — | — |
| POST | `/api/connectors/:id/disconnect` | — | — |
| GET | `/api/vault` | — | `unknown` |
| POST | `/api/vault` | `{ name, value, type? }` | — |
| DELETE | `/api/vault/:id` | — | — |
| GET | `/api/profile` | — | profile |
| PUT | `/api/profile` | `Record<string,unknown>` | profile |
| POST | `/api/profile/analyze-style` | `{ text }` | analysis |
| POST | `/api/profile/analyze-brand` | `{ description }` | analysis |
| POST | `/api/profile/research` | `{}` | research |
### 3.14 Costs / Telemetry / Team / Weaver / Audit
| Method | Path | Response |
|---|---|---|
| GET | `/api/costs` · `/api/cost/by-workspace` · `/api/cost/summary` | cost objects |
| GET | `/api/telemetry/status` | `{ enabled, totalEvents }` |
| POST | `/api/telemetry/toggle` | — (`{ enabled }`) |
| DELETE | `/api/telemetry/events` | `{ deleted }` |
| POST | `/api/telemetry/track` | — (`{ event, properties }`, fire-and-forget) |
| POST | `/api/team/connect` | — (`{ serverUrl, token }`) |
| POST | `/api/team/disconnect` | — |
| GET | `/api/team/status` | `{ connected, teamName? }` |
| GET | `/api/team/members` · `/api/team/activity` · `/api/team/messages?workspaceId=` | arrays |
| GET | `/api/weaver/status` | `{ lastConsolidation?, status }` |
| POST | `/api/weaver/trigger` | — (WeaverPanel, raw) |
| GET | `/api/audit/installs` | `unknown[]` |
### 3.15 Waggle Dance (multi-agent signals)
| Method | Path | Request | Response | Stream |
|---|---|---|---|---|
| GET | `/api/waggle/signals` | — | `WaggleSignal[]` | no |
| POST | `/api/waggle/signals` | `Omit<WaggleSignal,'id'\|'timestamp'>` | `WaggleSignal` | no |
| PATCH | `/api/waggle/signals/:id/ack` | — | — | no |
| — | `/api/waggle/stream` | — | `WaggleSignal` | **yes (SSE)** |
### 3.16 AI-OS Tool Launcher
| Method | Path | Request | Response |
|---|---|---|---|
| GET | `/api/tools/detect` | — | `{ platform, detectedAt, tools[{id,displayName,installed,installedPath,version,hooksInstalled,...}] }` |
| POST | `/api/tools/launch` | `{ id, installedPath, workspaceId?, args?, cwd? }` | `{ ok, pid, error? }` |
| GET | `/api/tools/processes` | — | `{ processes[{pid,toolId,startedAt,workspaceId?}], total }` |
| POST | `/api/tools/kill` | `{ pid }` | `{ ok, pid, reason, error? }` |
| POST | `/api/tools/hooks` | `{ id, action:'install'\|'verify'\|'uninstall', cliPath? }` | `{ ok, action, stdout, stderr, code, error? }` |
### 3.17 Billing / Tier / GDPR Erase / Trial
| Method | Path | Request | Response |
|---|---|---|---|
| GET | `/api/tier` | — | `{ tier, trialDaysRemaining?, trialExpired?, capabilities, usage }` |
| POST | `/api/tier/start-trial` | — | `{ tier, rawTier, trialStartedAt, trialDaysRemaining, trialExpired, capabilities }` (409 if started) |
| POST | `/api/stripe/sync` | `{ sessionId }` | `{ tier, customerId }` |
| POST | `/api/stripe/create-checkout-session` | `{ tier:'PRO'\|'TEAMS' }` | `{ url }` |
| POST | `/api/stripe/create-portal-session` | — | `{ url }` |
| POST | `/api/data/erase` | header `X-Confirm-Erase: yes` + `{ confirmation:'I UNDERSTAND THIS IS PERMANENT' }` | `{ requestedAt, markerPath, dataDirSnapshot, instruction }` |
### 3.18 Import / Harvest
| Method | Path | Request | Response | Stream |
|---|---|---|---|---|
| POST | `/api/import/preview` | `{ data, source }` | `{ knowledgeExtracted[] }` | no |
| POST | `/api/import/commit` | `{ data, source }` | — | no |
| POST | `/api/harvest/preview` | `{ data, source }` | preview | no |
| POST | `/api/harvest/commit` | `{ data, source }` / `{ resumeFromRun }` | result | no |
| GET | `/api/harvest/sources` | — | `{ sources[] }` | no |
| DELETE | `/api/harvest/sources/:source` | — | — | no |
| PATCH | `/api/harvest/sources/:source` | `{ autoSync }` | source | no |
| POST | `/api/harvest/scan-claude-code` | (bodyless) | scan | no |
| POST | `/api/harvest/extract-identity` | — | `{ suggestions[], note? }` | no |
| GET | `/api/harvest/runs/latest-interrupted` | — | `{ run \| null }` | no |
| POST | `/api/harvest/runs/:id/abandon` | — | — | no |
| — | `/api/harvest/progress` | — | `{ phase, current, total, source }` | **yes (SSE)** |
### 3.19 Wiki / Compliance
| Method | Path | Request | Response |
|---|---|---|---|
| GET | `/api/wiki/pages` · `/api/wiki/pages/:slug` · `/api/wiki/pages/:slug/content` | — | page(s) |
| POST | `/api/wiki/compile` | `{ mode:'incremental'\|'full', concepts? }` | result |
| GET | `/api/wiki/health` · `/api/wiki/watermark` | — | objects |
| POST | `/api/wiki/export/obsidian` | `{ outDir }` | `{ outDir, filesWritten, indexPath, byType }` |
| POST | `/api/wiki/export/notion` | `{ rootPageUrl }` | `{ pagesCreated, pagesUpdated, pagesUnchanged, pagesFailed, byType, errors[] }` |
| GET | `/api/compliance/status?workspaceId=` | — | status |
| POST | `/api/compliance/export` | request | json |
| POST | `/api/compliance/export-pdf` | request | `Blob` |
| GET | `/api/compliance/interactions?limit=` · `/api/compliance/models` | — | data |
| GET | `/api/compliance/templates` | — | `{ templates[] }` |
| POST | `/api/compliance/templates` | input | `{ template }` |
| PATCH | `/api/compliance/templates/:id` | patch | `{ template }` |
| DELETE | `/api/compliance/templates/:id` | — | — |
### 3.20 Endpoints called via raw `adapter.fetch` (no dedicated method)
These are hit directly inside components, bypassing a named adapter method:
| Path(s) | Caller |
|---|---|
| `/api/audit/installs`, `/api/skills/test` | `CapabilitiesApp.tsx` |
| `/api/backup/metadata`, `/api/backup`, `/api/restore` | `BackupApp.tsx`, `SettingsApp.tsx` |
| `/api/tasks` | `DashboardApp.tsx` |
| `/api/cost/by-workspace`, `/api/events/stats` | `TelemetryApp.tsx` |
| `/api/evolution/{runs,runs/:id,run,targets,baseline,status}` | `memory/EvolutionTab.tsx` |
| `/api/weaver/trigger` | `memory/WeaverPanel.tsx` |
| `/api/settings`, `/api/settings/permissions`, `/api/providers`, `/api/export`, `/api/debug/logs` | `SettingsApp.tsx` |
| `/api/v1/models`, `/api/litellm/models` | onboarding `ModelTierStep.tsx`, `SpawnAgentDialog.tsx` |
---
## 4. Feature → UI Component → Backend Endpoints
### 4.1 Dock Apps (windows opened via `appId`)
App registration lives in two places: the dock config (`lib/dock-tiers.ts`, by `UserTier`) and the window content switch (`Desktop.tsx` `renderAppContent`). The full `AppId` union (`lib/dock-tiers.ts`):
`chat`, `dashboard`, `memory`, `events`, `capabilities`, `connectors`, `cockpit`, `mission-control`, `settings`, `vault`, `profile`, `terminal`, `calculator`, `notes`, `waggle-dance`, `files`, `agents`, `scheduled-jobs`, `marketplace`, `voice`, `room`, `approvals`, `timeline`, `backup`, `telemetry`, `governance`, `launcher`.
| Feature | UI component | Backend endpoints used |
|---|---|---|
| **Chat** (token-streaming conversation, model switch, pins, file ingest, memory recall, feedback, approvals) | `apps/ChatApp.tsx` + `ChatWindowInstance.tsx` + `chat-blocks/*` (via `useChat`) | `POST /api/chat` (SSE), `GET /api/history`, `DELETE /api/chat/history`, `POST /api/approval/:id`, `submitFeedback`, `searchMemory`, pins (`GET/POST/DELETE /api/workspaces/:id/pins`), `ingestFile` (`/api/ingest`), `getModel/setModel` (`/api/agent/model`), `getModels`, `getSettings`, `getTeamMembers`, `patchWorkspace` |
| **Dashboard / Home** | `apps/DashboardApp.tsx` | `getMemoryStats` (`/api/memory/stats`), `GET /api/tasks` (raw), `getServerUrl` |
| **Memory** (tabs: Frames, Knowledge Graph, Harvest, Wiki, Evolution) | `apps/MemoryApp.tsx``memory/KnowledgeGraphViewer.tsx`, `HarvestTab.tsx`, `WikiTab.tsx`, `EvolutionTab.tsx`, `WeaverPanel.tsx`, `ImportReminderBanner.tsx` (via `useMemory`, `useKnowledgeGraph`) | frames CRUD (`/api/memory/frames*`), `searchMemory`, `getMemoryStats`, `getKnowledgeGraph`; **Harvest:** `getHarvestSources`, `scanClaudeCode`, `harvestPreview/Commit`, `getLatestInterruptedHarvestRun`, `resume/abandonHarvestRun`, `extractHarvestIdentity`, `subscribeHarvestProgress`, `toggleHarvestAutoSync`, `removeHarvestSource`; **Wiki:** `getWikiPages/PageContent`, `compileWiki`, `getWikiHealth`, `exportWikiToObsidian/Notion`; **Evolution:** `/api/evolution/{runs,run,targets,baseline,status}`; **Weaver:** `getWeaverStatus`, `POST /api/weaver/trigger` |
| **Events & Logs** | `apps/EventsApp.tsx` (via `useEvents`) | `getEvents` (`/api/events`), `subscribeEvents` (`/api/events/stream` SSE) |
| **Skills & Apps** (incl. Marketplace tab) | `apps/CapabilitiesApp.tsx` | `getSkills`, `getStarterPacks`, `getCapabilityPacks`, `getMarketplacePacks`, `installPack`, `installMarketplacePack`, `GET /api/audit/installs` + `/api/skills/test` (raw) |
| **Connectors** | `apps/ConnectorsApp.tsx` + `connectors/{McpCatalog,McpServerCard,BrandTile}.tsx` | `getConnectors`, `connectConnector`, `disconnectConnector`, `addVaultSecret` |
| **Cockpit / Command Center** (incl. Compliance dashboard) | `apps/CockpitApp.tsx` + `cockpit/{ComplianceDashboard,ComplianceTemplateModal}.tsx` | `getSystemHealth`, `getAgentCost`, `getConnectors`, `getCronJobs`, `getVault`, `getCapabilitiesStatus`, `getAuditInstalls`, `getCostSummary`, `getWeaverStatus`, `getEventStats`; **Compliance:** `getComplianceStatus`, `listComplianceTemplates`, `getHarvestSources`, `exportComplianceReport(+Pdf)`, template CRUD (`/api/compliance/templates*`) |
| **Mission Control** | `apps/MissionControlApp.tsx` | `getFleet`, `getTeamMembers`, `getTeamActivity`, `detectTools`, `fleetAction` |
| **Waggle Dance** | `apps/WaggleDanceApp.tsx` (via `useWaggleDance`) | `getWaggleSignals`, `subscribeWaggleDance` (`/api/waggle/stream` SSE), `publishWaggleSignal`, `acknowledgeWaggleSignal` |
| **Personas (Agents)** | `apps/AgentsApp.tsx` + `agents/{AgentCard,AgentDetail,CreateAgentForm,CreateGroupForm,GroupCard,GroupDetail,GroupExecutionPanel}.tsx` | `getPersonas`, `getCapabilityStatus`, `getAgentGroups`, `createPersona`, `updatePersona`, `deletePersona`, `createAgentGroup`, `updateAgentGroup`, `deleteAgentGroup`, `runAgentGroup`, `generatePersona`, `getJobStatus`, `cancelJob` |
| **Files** | `apps/FilesAppTabs.tsx``FilesApp.tsx` + `files/{FileTree,FilePreview,FileActions,FileUploadZone,SyntaxPreview,WorkspaceRail}.tsx` | `listFiles`, `uploadFile`, `downloadFile`, `createDirectory`, `deleteFile`, `moveFile`, `copyFile`, `getDocuments`, `getDocumentVersions` |
| **Scheduled Jobs** | `apps/ScheduledJobsApp.tsx` | `getCronJobs`, `createCronJob`, `updateCronJob`, `deleteCronJob`, `triggerCronJob` |
| **Marketplace** | `apps/MarketplaceApp.tsx` | `connect`, `searchMarketplace`, `getMarketplaceInstalled`, `installMarketplacePackage`, `uninstallMarketplacePackage` |
| **AI Tools / Launcher** | `apps/LauncherApp.tsx` | `detectTools`, `launchTool`, `manageHooks`, `getToolProcesses`, `killTool` |
| **Voice** | `apps/VoiceApp.tsx` | **none** — static "Coming Soon" placeholder |
| **Room** (live sub-agent canvas) | `apps/RoomApp.tsx` (via `useRoomState`) | `subscribeSubagentStatus` (`/api/notifications/stream`, named `subagent_status` SSE event) |
| **Approvals** | `apps/ApprovalsApp.tsx` | `getPendingApprovals`, `getApprovalGrants`, `respondApproval`, `revokeApprovalGrant`, `clearApprovalGrants` |
| **Timeline** | `apps/TimelineApp.tsx` | `getTimeline` (`/api/events?...&from=`) |
| **Backup & Restore** | `apps/BackupApp.tsx` | `GET /api/backup/metadata`, `POST /api/backup`, `POST /api/restore` (raw `adapter.fetch`) |
| **Usage & Telemetry** | `apps/TelemetryApp.tsx` | `GET /api/cost/by-workspace`, `GET /api/events/stats` (raw) |
| **Team Governance** (TEAMS tier) | `apps/TeamGovernanceApp.tsx` | **no direct adapter calls** (renders governance UI; sub-components / props supply data) |
| **Settings** | `apps/SettingsApp.tsx` | `getSettings`/`saveSettings`, `getPermissions`/`savePermissions`, `getTeamStatus`, `getTelemetryStatus`/`toggleTelemetry`/`clearTelemetry`, `teamConnect`/`teamDisconnect`, `GET /api/providers`, `/api/export`, `/api/backup`, `/api/restore`, `/api/debug/logs` (raw) |
| **Vault** | `apps/VaultApp.tsx` | `getVault`, `getConnectors`, `addVaultSecret`, `deleteVaultSecret`, `connectConnector`, `disconnectConnector`, `DELETE /api/vault/:id` |
| **My Profile** | `apps/UserProfileApp.tsx` | `getProfile`, `updateProfile`, `analyzeWritingStyle`, `analyzeBrand`, `researchProfile` |
> `terminal`, `calculator`, `notes` appear in the `AppId` union but have no `*App.tsx` component or `renderAppContent` case — they are declared-but-unimplemented placeholders. `voice` is implemented but is a static placeholder.
### 4.2 Overlays (modals, dialogs, rails — rendered directly by `Desktop.tsx`)
| Overlay | UI component | Backend endpoints used |
|---|---|---|
| Onboarding wizard (8 steps) | `overlays/OnboardingWizard.tsx` + `overlays/onboarding/{Welcome,WhyWaggle,Tier,ModelTier,Import,Template,Persona,ApiKey,Ready}Step.tsx` | `connect`, `trackTelemetry`, `getVault`, `getSystemHealth`, `getProviders`, `harvestPreview/Commit`, `scanClaudeCode`, `import/*`, `createPersona`, `addVaultSecret`, `createWorkspace`, `saveSettings`, `/api/v1/models` |
| Login briefing (session-start digest) | `overlays/LoginBriefing.tsx` | `getIdentity`, `getWorkspaces`, `searchMemory`, `getMemoryStats`, `getWorkspaceContext` |
| Global search (Cmd+K) | `overlays/GlobalSearch.tsx` | `getWorkspaces`, `getSessions`, `getSkills`, `searchMemory` |
| Create workspace dialog | `overlays/CreateWorkspaceDialog.tsx` | `browseLocal`, `browseLocalMkdir`, `getWorkspaceTemplates`, `createWorkspaceTemplate`, `updateWorkspaceTemplate`, `deleteWorkspaceTemplate`, `generateTemplateFromPrompt`, `getConnectors`, `getAgentGroups` |
| Persona switcher | `overlays/PersonaSwitcher.tsx` | `getPersonas`, `getAgentGroups` |
| Spawn agent dialog | `overlays/SpawnAgentDialog.tsx` | `getModels`, `getModelPricing`, `getProviders`, `getModel`, `createWorkspace`, `spawnAgent`, `/api/litellm/models` |
| Workspace switcher | `overlays/WorkspaceSwitcher.tsx` | (props-driven; workspaces from `useWorkspaces`) |
| Notification inbox | `overlays/NotificationInbox.tsx` | (props from `useNotifications`) |
| Context rail | `overlays/ContextRail.tsx` | uses `lib/context-rail-fetch.ts` (memory/context lookups) |
| Erase data dialog (GDPR) | `overlays/EraseDataDialog.tsx` | `eraseData` (`POST /api/data/erase`) |
| Upgrade modal | `overlays/UpgradeModal.tsx` | (triggered by `waggle:tier-insufficient` event; actions call `startTrial`, `createCheckoutSession`) |
| Trial expired modal | `overlays/TrialExpiredModal.tsx` | `createCheckoutSession` |
| Keyboard shortcuts help | `overlays/KeyboardShortcutsHelp.tsx` | none |
| Onboarding tooltips / tour | `overlays/OnboardingTooltips.tsx` | none |
---
## 5. Global Providers & State
There is **no Redux / Zustand / React Query**. State is React Context + custom hooks + the singleton `adapter`.
### 5.1 Provider tree (`apps/web/src/App.tsx`)
`App.tsx` wraps the router in `<ServiceProvider>` plus shadcn `<TooltipProvider>` and a `<Toaster>` (toast system in `hooks/use-toast.ts`). The only domain provider is `ServiceProvider`.
| Provider | File | Provides |
|---|---|---|
| `ServiceProvider` / `useService()` | `providers/ServiceProvider.tsx` | `{ adapter, connected, connecting, error, reconnect }`; calls `adapter.connect()` once on mount |
### 5.2 Domain hooks (the de-facto "store")
Each hook wraps adapter calls + local `useState`. `Desktop.tsx` composes them.
| Hook | File | Owns / returns |
|---|---|---|
| `useWorkspaces` | `hooks/useWorkspaces.ts` | workspaces list, active workspace, `selectWorkspace`, `createWorkspace`, `patchWorkspace`, `deleteWorkspace`, `refresh` |
| `useChat` | `hooks/useChat.ts` | `messages`, `isLoading`, `sendMessage`, `clearHistory`, `pendingApproval`, `approveAction`; parses SSE stream into `ContentBlock[]` |
| `useSessions` | `hooks/useSessions.ts` | per-workspace sessions CRUD |
| `useMemory` | `hooks/useMemory.ts` | frames, stats, search, add/update/delete, `incrementFrameAccess` |
| `useKnowledgeGraph` | `hooks/useKnowledgeGraph.ts` | `{ nodes, edges }` via `getKnowledgeGraph` |
| `useEvents` | `hooks/useEvents.ts` | steps + live `subscribeEvents` SSE |
| `useNotifications` | `hooks/useNotifications.ts` | notifications, `unreadCount`, `markRead`, `markAllRead`, live SSE |
| `useRoomState` | `hooks/useRoomState.ts` | live sub-agent map via `subscribeSubagentStatus` |
| `useWaggleDance` | `hooks/useWaggleDance.ts` | signals, filter, `acknowledge`, `publish`, live SSE |
| `useAgentStatus` | `hooks/useAgentStatus.ts` | `getAgentStatus` polling |
| `useBilling` | `hooks/useBilling.ts` | tier, `startCheckout`, `openPortal`, `syncAfterCheckout` (auto-detects `?session_id=`) |
| `useFeatureGate` | `hooks/useFeatureGate.ts` | `{ planTier, isEnabled, gate }` from `lib/feature-gates.ts` |
| `useOnboarding` | `hooks/useOnboarding.ts` | persisted `OnboardingState` in `localStorage["waggle:onboarding"]` (`completed, step, tier, workspaceId, apiKeySet, templateId, personaId, tooltipsDismissed`); supports `?skipOnboarding=true`, `?forceWizard=true` |
| `useOfflineStatus` | `hooks/useOfflineStatus.ts` | polls `getSystemHealth` for the offline pill |
| `useProviders` | `hooks/useProviders.ts` | `getProviders` (`/api/providers`) |
| `useWindowManager` | `hooks/useWindowManager.ts` | window open/close/focus/minimize, per-window persona & autonomy |
| `useOverlayState` | `hooks/useOverlayState.ts` | boolean flags for every overlay |
| `useKeyboardShortcuts` | `hooks/useKeyboardShortcuts.ts` | global hotkeys → overlay/app open |
| `useDeveloperMode`, `useDockNudge`, `useDockLabels` | `hooks/use*.ts` | UI affordances |
### 5.3 Cross-component event bus (`window` CustomEvents)
The UI also coordinates through DOM events (no library). A rebuild must wire these:
| Event | Dispatched by | Consumed by |
|---|---|---|
| `waggle:tier-insufficient` (`{ required, actual, message }`) | `adapter.fetch` on 403 `TIER_INSUFFICIENT` | `UpgradeModal` |
| `waggle:open-app` (`{ appId, tab? }`) | `HarvestTab` and others | `Desktop.tsx``wm.openApp(appId)` |
---
## 6. Tier / Billing Gating (drives dock & feature visibility)
- **Dock visibility** is computed by `getDockForTier(tier, billingTier)` (`lib/dock-tiers.ts`). `UserTier` = `'simple' | 'professional' | 'power' | 'admin'` (UI complexity); `BillingTier` = `'TRIAL' | 'FREE' | 'PRO' | 'TEAMS' | 'ENTERPRISE'`. Entries with `minBillingTier` (e.g. `governance` and `approvals` = `TEAMS`) are filtered out below that tier; empty zone-parents are dropped.
- **Feature gating** uses `useFeatureGate()``lib/feature-gates.ts` (`isFeatureEnabled`, `getGate`, `dockTierToPlanTier`).
- **Trial / upgrade flow:** `Desktop.tsx` calls `getTier` on mount; `startTrial``POST /api/tier/start-trial`; checkout via `createCheckoutSession`; post-checkout sync via `useBilling` detecting `?session_id=` then `POST /api/stripe/sync`.
---
## 7. Rebuild Checklist (what Lovable must recreate, in order)
1. A single `adapter` module pointing at `http://127.0.0.1:3333`, with: server-url override in `localStorage["waggle:server-url"]`, session-token bootstrap (`GET /api/auth/session-token`), `Authorization: Bearer` injection, 403→`waggle:tier-insufficient` event, 10s/30s timeouts, and the read normalizers in §2.4.
2. `ServiceProvider` calling `adapter.connect()` once; expose `useService()`.
3. The window-manager desktop shell (open windows by `AppId`, dock per tier).
4. SSE plumbing for chat (`POST /api/chat`), events, notifications (+ named `subagent_status`), waggle-dance, harvest-progress; WS optional.
5. The 24 implemented apps + 13 overlays, each calling the endpoints in §4.
6. The cross-component `window` event bus (§5.3) and the tier-gating logic (§6).
---
## Counts
- **Endpoints documented:** ~135 distinct backend routes (across §3.1§3.20).
- **Apps documented:** 24 implemented dock apps (+3 declared-but-unimplemented: `terminal`, `calculator`, `notes`).
- **Overlays documented:** 13.
- **Pages:** 2 (`Index`, `NotFound`).
- **Domain hooks documented:** 19.
- **Providers:** 1 (`ServiceProvider`).