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

448 lines
36 KiB
Markdown
Raw 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.
# 07 — FRONTEND REBUILD GUIDE (Lovable)
> **What this is.** The action-oriented, build-in-order playbook for rebuilding the Waggle OS web UI
> in Lovable against the **existing, unchanged Fastify sidecar**. The backend is the contract; the
> frontend is replaceable. Everything below is grounded in the backend map sections (`02c`, `03a03g`,
> `04`) and the live `apps/web/src/` source. Field names, paths, ports, and tokens are quoted verbatim.
>
> **Companion docs (read alongside):**
> - `sections/04-feature-map.md` — full app↔endpoint matrix (the canonical screen list).
> - `sections/02c-shared-types-tiers.md` — wire types, Zod request schemas, the 5-tier capability matrix.
> - `sections/03a-api-chat-agents.md` — chat SSE event catalogue + approvals.
> - `sections/03f-api-realtime-ops.md` — the 4 SSE streams + ops endpoints.
> - `sections/03g-api-cloud-billing-kvark.md` — auth handshake, guards, Stripe.
---
## 0. The 60-second mental model
Waggle's UI is **a single-page "desktop OS"**, not a multi-route app. React Router has exactly two routes
(`/``<Index>`, `*``<NotFound>`). `Index` shows a boot screen then mounts `<Desktop>`, which **is** the
shell: it owns a window manager, a Dock, draggable app windows, and overlays. Apps are opened by `appId`,
not by URL. Everything talks to the backend through **one singleton adapter** pointed at the local Fastify
sidecar.
```
Lovable App
└─ ServiceProvider (calls adapter.connect() once, exposes useService())
└─ Desktop shell
├─ Dock (tier-filtered app launcher)
├─ WindowManager (open/close/focus windows by appId)
│ └─ AppWindow × N → renderAppContent(appId) → <XxxApp />
└─ Overlays (modals, rails, wizards)
── all of the above import the SAME `adapter` singleton ──
└─ adapter → http://127.0.0.1:3333 (Fastify sidecar)
```
Build the adapter + ServiceProvider + Desktop shell **first**. Everything else is screens that call adapter
methods.
---
## 1. API base URL, transport, auth, headers, tier pattern
### 1.1 Base URL
| Concern | Value / behavior |
|---|---|
| Default server | `http://127.0.0.1:3333` (constant `DEFAULT_SERVER`) |
| Override | `localStorage["waggle:server-url"]`, settable via `adapter.setServerUrl(url)` |
| Resolution order | `ctorArg ?? localStorage["waggle:server-url"] ?? DEFAULT_SERVER` |
| Prefix | **None.** Every route hardcodes its own full path (`/api/...`, `/health`, `/ws`, `/v1/...`). Do **not** add a base prefix. |
| Same-origin reality | In the desktop binary the SPA is served BY the sidecar, so the page origin IS the API root (`http://localhost:3333` or `tauri://localhost`). For a Lovable web rebuild, hit `http://127.0.0.1:3333` explicitly; expect CORS to be pre-allowlisted for `localhost:5173/8080/8081/8082/3333/1420` and `tauri://localhost`. |
### 1.2 Auth handshake (two-step, no login form)
There is **no username/password UI** for the local sidecar. Auth is a boot-generated session token:
1. **Static shell loads token-free** — non-API GETs are auth-exempt.
2. **Fetch token once on connect:** `GET /api/auth/session-token``{ token }`. This route is auth-exempt
but **same-origin gated**. Store it as `authToken`.
3. **Send Bearer on everything else:** every `/api/*` request must carry
`Authorization: Bearer <token>`. Missing → `401 MISSING_TOKEN`; wrong → `401 INVALID_TOKEN`.
4. **WebSocket** (optional) uses the token in the query string: `GET /ws?token=<authToken>` (not a header).
Wrong token closes the socket with code `4001`.
```
connect():
GET /health → probe (fallback to DEFAULT_SERVER once if stored URL fails; persist working URL)
GET /api/auth/session-token → { token }; this.authToken = token
set _connected = true
```
### 1.3 The single `fetch` wrapper (reproduce exactly)
Every request goes through one `adapter.fetch(path, init)`. It MUST:
- Add `Content-Type: application/json` **only when a body is present** and no content-type was supplied.
(Body-less POSTs must NOT send a JSON content-type — this is a deliberate fix; sending it breaks some routes.)
- Add `Authorization: Bearer <authToken>` when the token is set.
- On HTTP **403** with body `{ error: 'TIER_INSUFFICIENT' }`, dispatch a global DOM event
`window.dispatchEvent(new CustomEvent('waggle:tier-insufficient', { detail: { required, actual, message } }))`.
This is the **only** trigger for the Upgrade modal.
- Use a timeout: **10s default, 30s for upload/ingest/harvest**. Throw `TimeoutError` / `NetworkError`.
- Handle rate limits: the sidecar enforces sliding-window limits (default **100/min**; `/api/chat` 120/min;
`/api/vault/*/reveal` 5/min; `/api/backup` & `/api/restore` 2/min; `/api/browse/local/mkdir` 10/min).
On `429`, read `Retry-After` and back off.
### 1.4 Read-side normalizers (reproduce or the UI crashes on `undefined`)
The backend and UI disagree on several field names. The adapter normalizes on read; you must too:
| Helper | Mapping |
|---|---|
| `unwrapArray<T>(data)` | accepts a raw array OR `{ results: [...] }` / `{ <key>: [...] }` envelope |
| `normalizeFrame(raw)` | frameType code `I/F/E/D/T/N``insight/fact/event/decision/task/entity`; `importance` string ↔ number 14 |
| `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`; tolerate `workspace:null` |
| `getModel()` | accepts raw `string` OR `{ model }` |
> **Tauri dual-path (skip for Lovable web).** A few memory methods (`addMemoryFrame`, `searchMemory`,
> `getKnowledgeGraph`, `getIdentity`) branch on `isTauri()` and use Rust IPC. A web rebuild always takes the
> `else` HTTP branch — ignore the IPC path entirely.
### 1.5 Tier gating pattern (read this before gating any feature)
- Fetch the user's tier from `GET /api/tier``{ tier, trialDaysRemaining?, trialExpired?, capabilities, usage }`.
- **Always run the stored tier through `getEffectiveTier(tier, trialStartedAt)` before reading
capabilities** — an expired TRIAL must collapse to FREE gating. (`getEffectiveTier`, `getCapabilities`,
`hasCapability` live in `@waggle/shared/tiers`; port them or reimplement.)
- Gate UI on `TierCapabilities` flags, not on the tier name. `-1` means **unlimited** (short-circuits to allowed).
- The **upgrade trigger is Skills + Connectors + Team features** — never agents/memory (`spawnAgents` is `true`
in every tier; memory/embedding quotas are `-1` everywhere). The full matrix is in `02c §15.2`.
- Two distinct tier axes exist:
- `BillingTier` = `'TRIAL'|'FREE'|'PRO'|'TEAMS'|'ENTERPRISE'` (entitlements).
- `UserTier` = `'simple'|'professional'|'power'|'admin'` (UI density / dock complexity).
- The Dock is computed from both via `getDockForTier(userTier, billingTier)`.
---
## 2. Real-time: which endpoints stream, and how to consume each
There are **five** streaming surfaces. Four are SSE (`EventSource`), one is the chat POST-SSE hybrid, plus an
optional WebSocket. **The chat stream is a POST, so you cannot use `EventSource` for it** — you parse the
response body manually.
### 2.1 Chat token stream — `POST /api/chat` (SSE-formatted body)
- **Not** a JSON endpoint and **not** `EventSource`-compatible (EventSource is GET-only). Issue a `fetch` POST,
read `response.body.getReader()`, decode, and split on `\n\n`; each frame is `event: <name>\ndata: <json>`.
- All validation/auth happen **before** the server hijacks the reply. So a `400`/`403` comes back as normal
JSON; anything after is SSE. Check `response.ok` / content-type before entering the stream loop.
- **Request body:** `{ message (required), workspace?/workspaceId?, model?, session?, persona?, autonomy?, shape? }`.
`autonomy = { level: 'normal'|'trusted'|'yolo', expiresAt? }`.
- **Event catalogue** (handle ALL of these):
| `event:` | `data` | Action |
|---|---|---|
| `token` | `{ content }` | append to streaming assistant text |
| `step` | `{ content }` | render a progress line ("Recalling memories…", budget notes) |
| `tool` | `{ name, input }` | show tool-call block (adapter normalizes name `tool``tool_start`) |
| `tool_result` | `{ name, result, duration?, isError }` | close tool block (normalized `tool_result``tool_end`) |
| `file_created` | `{ filePath, fileAction: 'write'|'edit'|'generate' }` | show "file created" affordance |
| `approval_required` | `{ requestId, toolName, input, sourceWorkspaceId, ...trustMeta }` | **PAUSE.** Render approve/deny; resolve via `POST /api/approval/:requestId` |
| `gepa_choices` | `{ original, expanded, clarifyingQuestions[], intent }` | offer ask-first clarification |
| `model_switch` | `{ model, reason, primary }` | toast "switched to <model>" |
| `notification` | `{ type:'workflow_captured', title, message, pattern }` | suggest saving a workflow |
| `done` | `{ content, usage{inputTokens,outputTokens}, toolsUsed[], model, cost?, tokens? }` | **terminal success** |
| `error` | `{ message }` | **terminal failure** (user-friendly only) |
Tolerate two `done` usage shapes: agent-loop `{ inputTokens, outputTokens }` and echo/command-path
`{ prompt_tokens, completion_tokens, total_tokens }`.
- **Approvals are blocking.** When `approval_required` arrives the agent is paused awaiting a server-side
Promise (auto-denies after **5 min**). POST `{ approved: boolean, always?: boolean, reason?, sourceWorkspaceId }`
to `/api/approval/:requestId`, echoing `sourceWorkspaceId` verbatim. `always:true` persists a grant.
### 2.2 The four GET SSE streams (`EventSource`)
| Endpoint | How to subscribe | Events you read |
|---|---|---|
| `GET /api/waggle/stream` | `new EventSource(url)` + `addEventListener('signal', …)` | `signal` → full `WaggleSignal` JSON. Initial `event: connected`. Heartbeat `: heartbeat` every 30s. |
| `GET /api/events/stream` | `addEventListener('audit', …)` | `audit` → full `AuditEvent` JSON. Initial `data:{"type":"connected"}`. |
| `GET /api/notifications/stream` | **mixed:** `onmessage` for unnamed frames + `addEventListener('subagent_status'|'workflow_suggestion', …)` | `notification` arrives as an **unnamed** `data:` frame (use `onmessage`); `subagent_status` and `workflow_suggestion` are **named** events. |
| `GET /api/harvest/progress` | `new EventSource(url)` | `{ phase, current, total, source }` progress frames. Adapter wraps as `{ ready: Promise, close }`. |
**EventSource caveat:** native `EventSource` cannot set an `Authorization` header. The sidecar gates these
streams by **same-origin** (the SPA is same-origin in production). For a cross-origin Lovable dev build you may
need to either (a) run behind a same-origin dev proxy to the sidecar, or (b) use a fetch-stream polyfill that
injects the Bearer header. The streams set `Access-Control-Allow-Origin` only for allowlisted origins.
### 2.3 WebSocket (optional) — `GET /ws?token=<authToken>`
Local sidecar `/ws` is an event-bus relay. Server→client frames `{ event, data }` for
`approval_required | step | tool | done | error | presence_update | notification`. Client→server
`{ type: 'approve'|'deny', requestId }`. The SSE streams already cover the UI's needs, so WS is **optional** for
the rebuild. (The cloud-server `/ws` is a separate Clerk+Redis team-chat gateway — not used by the local UI.)
### 2.4 Hook pattern for streams
Mirror the existing domain hooks: each owns local state + opens its stream on mount, returns close on unmount.
```ts
// useEvents → subscribeEvents (/api/events/stream)
// useNotifications → /api/notifications/stream (unread count, markRead, markAllRead)
// useRoomState → subscribeSubagentStatus (named 'subagent_status' on /api/notifications/stream)
// useWaggleDance → subscribeWaggleDance (/api/waggle/stream) + publish/ack
// useChat → POST /api/chat fetch-stream → parses into ContentBlock[]
```
---
## 3. Global state & providers the rebuild needs
**There is no Redux / Zustand / React Query.** State = React Context (one provider) + custom hooks + the
singleton adapter + a `window` CustomEvent bus. Reproduce these four layers.
### 3.1 The one provider
| Provider | Provides | Behavior |
|---|---|---|
| `ServiceProvider` / `useService()` | `{ adapter, connected, connecting, error, reconnect }` | calls `adapter.connect()` **once** on mount; wrap the whole app. (Plus shadcn `TooltipProvider` + a toast `Toaster`.) |
### 3.2 Domain hooks (the de-facto store)
Recreate these as the state layer — each wraps adapter calls + `useState`. The Desktop shell composes them.
| Hook | Owns |
|---|---|
| `useWorkspaces` | workspaces, **active workspace**, select/create/patch/delete/refresh — the workspace context every other call needs |
| `useChat` | `messages` (as `ContentBlock[]`), `isLoading`, `sendMessage`, `clearHistory`, `pendingApproval`, `approveAction`; parses the chat SSE stream |
| `useSessions` | per-workspace session CRUD (list/create/rename/delete/search/export) |
| `useMemory` | frames, stats, search, add/update/delete, `incrementFrameAccess` |
| `useKnowledgeGraph` | `{ nodes, edges }` |
| `useEvents` | audit steps + live SSE |
| `useNotifications` | notifications, `unreadCount`, markRead/markAllRead, live SSE |
| `useRoomState` | live sub-agent map (named `subagent_status` SSE) |
| `useWaggleDance` | signals, filter, ack, publish, live SSE |
| `useAgentStatus` | polls `/api/agent/status` |
| `useBilling` | **tier**, `startCheckout`, `openPortal`, `syncAfterCheckout` (auto-detects `?session_id=` on load) |
| `useFeatureGate` | `{ planTier, isEnabled, gate }` — the per-feature gate front-end |
| `useOnboarding` | persisted `OnboardingState` in `localStorage["waggle:onboarding"]` (`completed, step, tier, workspaceId, apiKeySet, templateId, personaId, tooltipsDismissed`); honors `?skipOnboarding=true`, `?forceWizard=true` |
| `useOfflineStatus` | polls `/health` for the offline pill |
| `useProviders` | `/api/providers` |
| `useWindowManager` | window open/close/focus/minimize + **per-window persona & autonomy** |
| `useOverlayState` | boolean flags for every overlay |
| `useKeyboardShortcuts` | global hotkeys → open app/overlay (Cmd+K global search, etc.) |
### 3.3 The four pieces of "global context" the rebuild must thread
1. **Workspace context**`useWorkspaces.active`. Nearly every call accepts `workspace`/`workspaceId`
(alias-accepted; omit ⇒ `personal` mind). The active workspace id flows into chat, memory, files, sessions,
events. The window manager can override **persona** and **autonomy** per-window.
2. **Session** — the boot session token (`adapter.authToken`) for transport auth, plus the per-workspace
chat session id (`useSessions`, defaults to the workspace id then `'default'`).
3. **Persona** — workspace-default persona, overridable per-chat-window. Passed as `persona` in the chat body.
4. **Tier**`useBilling.tier``getEffectiveTier` → capabilities, consumed by `useFeatureGate`, the Dock, and
the Upgrade/TrialExpired modals.
### 3.4 Cross-component `window` event bus (no library — 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? }` | apps (e.g. `HarvestTab`) | `Desktop``wm.openApp(appId)` |
---
## 4. Screen-by-screen data contract
Each row = an OS app/overlay → the adapter methods/endpoints it calls → request/response summary. Paths are
verbatim and all `/api/*` are Bearer-gated. The `AppId` union (window content switch) is:
`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`.
(`terminal`/`calculator`/`notes` are declared but unimplemented; `voice` is a static "Coming Soon" placeholder.)
### 4.1 Dock apps
| Screen (appId) | Key endpoints | Request → Response summary |
|---|---|---|
| **Chat** (`chat`) | `POST /api/chat` (SSE); `GET /api/history?workspace=&session=`; `DELETE /api/chat/history?session=`; `POST /api/approval/:id`; `POST /api/feedback`; `GET /api/memory/search?q=&scope=`; pins `GET/POST/DELETE /api/workspaces/:id/pins`; `POST /api/ingest`; `GET/PUT /api/agent/model`; `GET /api/settings`; `GET /api/team/members`; `PATCH /api/workspaces/:id` | Send `{message,workspace,session?,persona?,autonomy?,shape}` → SSE stream (§2.1). History → `{ sessionId, messages[{id,role,content,timestamp}], count }`. Feedback `{sessionId,messageIndex,rating,reason?,detail?}` → fire-and-forget. Pin `{messageContent,messageRole,label?}`. Model PUT `{model}`. |
| **Dashboard / Home** (`dashboard`) | `GET /api/memory/stats`; `GET /api/tasks` (raw) | stats → `{ personal, workspace, total }` each `{frames,entities,relations}`. Tasks → task list. |
| **Memory** (`memory`) — tabs: Frames, Knowledge Graph, Harvest, Wiki, Evolution | frames `GET/POST/PUT/DELETE /api/memory/frames`, `PATCH …/:id/access?workspace=`; `GET /api/memory/search?q=&scope=`; `GET /api/memory/graph?workspace=` (or `?scope=all\|personal`); `GET /api/memory/stats`; **Harvest:** `GET /api/harvest/sources`, `POST /api/harvest/scan-claude-code`, `POST /api/harvest/preview`, `POST /api/harvest/commit`, `GET /api/harvest/runs/latest-interrupted`, `POST /api/harvest/runs/:id/abandon`, `POST /api/harvest/extract-identity`, `GET /api/harvest/progress` (SSE), `PATCH/DELETE /api/harvest/sources/:source`; **Wiki:** `GET /api/wiki/pages`, `…/:slug`, `…/:slug/content`, `POST /api/wiki/compile`, `GET /api/wiki/health`, `POST /api/wiki/export/{obsidian,notion}`; **Evolution:** `GET/POST /api/evolution/{runs,runs/:id,run,targets,baseline,status}` (raw); **Weaver:** `GET /api/weaver/status`, `POST /api/weaver/trigger` | Frame (normalized) `{id,content,source,frameType,importance,timestamp,score?,gop,accessCount,…}`. Graph → `{ nodes:KGNode[], edges:KGEdge[] }`. Harvest preview `{data,source}``{ knowledgeExtracted[] }`; commit `{data,source}` / `{resumeFromRun}`. Wiki compile `{ mode:'incremental'\|'full', concepts? }`. |
| **Events & Logs** (`events`) | `GET /api/events?workspaceId=`; `GET /api/events/stream` (SSE) | `AgentStep[]` + live `audit` events `{id,timestamp,workspaceId,eventType,toolName?,input?,output?,model?,cost?,…}`. |
| **Skills & Apps** (`capabilities`) incl. Marketplace tab | `GET /api/skills`; `POST /api/skills/create`; `GET /api/skills/starter-pack/catalog`; `POST /api/skills/starter-pack/:skillId`; `GET /api/skills/capability-packs/catalog`; `GET /api/marketplace/packs`; `POST /api/marketplace/install`; `GET /api/audit/installs` + `GET /api/skills/test` (raw) | Skills `SkillPack[]` (forced `installed:true`). Install `{packageId}` → raw `Response` (403 → UpgradeModal). Create `{name,description}`. |
| **Connectors** (`connectors`) | `GET /api/connectors`; `GET /api/connectors/:id/health`; `POST /api/connectors/:id/connect`; `POST /api/connectors/:id/disconnect`; `POST /api/vault` | `ConnectorDefinition[]` (card: `{id,name,description,service,authType,status,capabilities[],substrate,tools[],logoUrl?,category?,setupGuide?}`). Add secret `{name,value,type?}`. |
| **Cockpit / Command Center** (`cockpit`) incl. Compliance | `GET /health`; `GET /api/agent/cost`; `GET /api/connectors`; `GET /api/cron`; `GET /api/vault`; `GET /api/capabilities/status`; `GET /api/audit/installs`; `GET /api/cost/summary`; `GET /api/weaver/status`; `GET /api/events/stats`; **Compliance:** `GET /api/compliance/status?workspaceId=`, `GET /api/compliance/templates`, `POST/PATCH/DELETE /api/compliance/templates*`, `POST /api/compliance/export`, `POST /api/compliance/export-pdf` (Blob) | health `{status,uptime,services[]}`; cost summary; event stats `{totalEvents,period,byType,byDay,topTools}`. |
| **Mission Control** (`mission-control`) | `GET /api/fleet`; `GET /api/team/members`; `GET /api/team/activity`; `GET /api/tools/detect`; `POST /api/fleet/:workspaceId/(pause\|resume\|kill)` | Fleet (normalized) `{workspaceId,workspaceName,personaId,model,status,lastActivity,duration,toolCount,tokenUsage,costEstimate}` + `{count,maxSessions}`. |
| **Waggle Dance** (`waggle-dance`) | `GET /api/waggle/signals`; `GET /api/waggle/stream` (SSE); `POST /api/waggle/signals`; `PATCH /api/waggle/signals/:id/ack` | `WaggleSignal{id,type,workspaceId,content,metadata?,timestamp,acknowledged}`. Publish `{type,content,workspaceId?,metadata?}`. |
| **Personas (Agents)** (`agents`) | `GET /api/personas`; `POST /api/personas`; `PATCH/DELETE /api/personas/:id`; `POST /api/personas/generate`; `GET /api/capabilities/status`; `GET /api/agent-groups`; `POST /api/agent-groups`; `PATCH/DELETE /api/agent-groups/:id`; `POST /api/agent-groups/:id/run`; `GET /api/jobs/:jobId`; `POST /api/jobs/:jobId/cancel` | Persona create `{name,description,icon?,systemPrompt,tools?}`. Generate `{prompt}``{name,description,systemPrompt,tools[]}`. Group create `{name,description?,strategy:'parallel'\|'sequential'\|'coordinator',members[{agentId,roleInGroup:'lead'\|'worker',executionOrder}]}`. Group run `{task,teamId?}` → queued stub. |
| **Files** (`files`) | `GET /api/workspaces/:id/files/list?path=`; `POST …/files/upload` (FormData, 30s); `GET …/files/download?path=` (Blob); `POST …/files/{mkdir,delete,move,copy}`; `GET …/documents`; `GET …/documents/:name/versions` | `FileEntry[]`. Move/copy `{from,to}`; delete/mkdir `{path}`. Upload `FormData(file,path)`. |
| **Scheduled Jobs** (`scheduled-jobs`) | `GET /api/cron`; `POST /api/cron`; `PUT /api/cron/:id`; `DELETE /api/cron/:id`; `POST /api/cron/:id/trigger` | CronJob (normalized) `{id,name,schedule,jobType,jobConfig,workspaceId,enabled,lastRun,nextRun,createdAt}`. Create `{name,cronExpr,jobType,jobConfig?,workspaceId?,enabled?}`. Trigger → `{triggered,autoEnabled?,schedule?}`. |
| **Marketplace** (`marketplace`) | `GET /api/marketplace/search?query=&limit=`; `GET /api/marketplace/installed`; `POST /api/marketplace/install`; `POST /api/marketplace/uninstall` | search/install/uninstall return **raw `Response`** for 403-aware handling. |
| **AI Tools / Launcher** (`launcher`) | `GET /api/tools/detect`; `POST /api/tools/launch`; `GET /api/tools/processes`; `POST /api/tools/kill`; `POST /api/tools/hooks` | detect → `{platform,detectedAt,tools[{id,displayName,installed,installedPath,version,hooksInstalled,…}]}`. Launch `{id,installedPath,workspaceId?,args?,cwd?}``{ok,pid,error?}`. Hooks `{id,action:'install'\|'verify'\|'uninstall',cliPath?}`. |
| **Room** (`room`) | `GET /api/notifications/stream` named `subagent_status` (SSE) | live sub-agent canvas `{agents[{id,name,role,status:'pending'\|'running'\|'done'\|'failed',task,toolsUsed,…}]}`. |
| **Approvals** (`approvals`, TEAMS+) | `GET /api/approval/pending`; `GET /api/approval/grants`; `POST /api/approval/:id`; `DELETE /api/approval/grants/:id`; `POST /api/approval/grants/clear` | pending `{pending[{requestId,toolName,input,timestamp}],count}`. |
| **Timeline** (`timeline`) | `GET /api/events?workspaceId=&limit=&from=` | `TimelineEvent[]`. |
| **Backup & Restore** (`backup`) | `GET /api/backup/metadata`; `POST /api/backup`; `POST /api/restore` (all raw) | backup streams `application/octet-stream` (`.waggle-backup`, ≤500MB). Restore `{ backup:<base64>, preview? }`. |
| **Usage & Telemetry** (`telemetry`) | `GET /api/cost/by-workspace`; `GET /api/events/stats` (raw) | cost-by-workspace + stats aggregates. |
| **Team Governance** (`governance`, TEAMS+) | none direct (props-driven sub-components) | renders governance UI. |
| **Settings** (`settings`) | `GET/PUT /api/settings`; `GET/PUT /api/settings/permissions`; `POST /api/settings/test-key`; `GET /api/providers`; `GET /api/telemetry/status`; `POST /api/telemetry/toggle`; `DELETE /api/telemetry/events`; `GET /api/team/status`; `POST /api/team/{connect,disconnect}`; `GET /api/export`, `/api/debug/logs` (raw) | Settings object; permissions `{defaultAutonomy,externalGates[],workspaceOverrides}`. Test key `{provider,apiKey}``{valid}`. |
| **Vault** (`vault`) | `GET /api/vault`; `POST /api/vault`; `DELETE /api/vault/:id`; `GET /api/connectors`; `POST /api/connectors/:id/(connect\|disconnect)` | secret `{name,value,type?}`. Vault reveal is rate-limited to 5/min. |
| **My Profile** (`profile`) | `GET/PUT /api/profile`; `POST /api/profile/analyze-style` `{text}`; `POST /api/profile/analyze-brand` `{description}`; `POST /api/profile/research` `{}` | profile object + analysis results. |
### 4.2 Overlays (rendered by Desktop, not windowed)
| Overlay | Key endpoints |
|---|---|
| **Onboarding wizard** (8 steps: Welcome, WhyWaggle, Tier, ModelTier, Import, Template, Persona, ApiKey, Ready) | `connect`, `GET /api/vault`, `/health`, `GET /api/providers`, `/api/v1/models`, harvest preview/commit, `POST /api/harvest/scan-claude-code`, import preview/commit, `POST /api/personas`, `POST /api/vault`, `POST /api/workspaces`, `PUT /api/settings` |
| **Login briefing** (session-start digest) | `GET /api/identity`, `GET /api/workspaces`, `GET /api/memory/search`, `GET /api/memory/stats`, `GET /api/workspaces/:id/context` |
| **Global search (Cmd+K)** | `GET /api/workspaces`, sessions, `GET /api/skills`, `GET /api/memory/search` |
| **Create workspace dialog** | `GET /api/browse/local?path=`, `POST /api/browse/local/mkdir`, `GET/POST/PUT/DELETE /api/workspace-templates*`, `POST /api/workspace-templates/generate`, `GET /api/connectors`, `GET /api/agent-groups` |
| **Persona switcher** | `GET /api/personas`, `GET /api/agent-groups` |
| **Spawn agent dialog** | `GET /api/litellm/models`, `/api/litellm/pricing`, `GET /api/providers`, `GET /api/agent/model`, `POST /api/workspaces`, `POST /api/fleet/spawn` (`{task,persona?,model?,parentWorkspaceId?}`) |
| **Erase data dialog (GDPR)** | `POST /api/data/erase` — header `X-Confirm-Erase: yes` + body `{ confirmation:'I UNDERSTAND THIS IS PERMANENT' }` |
| **Upgrade modal** | triggered by `waggle:tier-insufficient`; actions → `POST /api/tier/start-trial`, `POST /api/stripe/create-checkout-session` `{tier:'PRO'\|'TEAMS', billingPeriod?}``{url}` |
| **Trial expired modal** | `POST /api/stripe/create-checkout-session` |
| Workspace switcher / Notification inbox / Context rail / Keyboard help / Tooltips | props-driven or `lib/context-rail-fetch.ts`; no/minor direct calls |
### 4.3 Billing / tier endpoints (used across overlays + `useBilling`)
| Method | Path | Body → Response |
|---|---|---|
| GET | `/api/tier` | → `{ tier, trialDaysRemaining?, trialExpired?, capabilities, usage }` |
| POST | `/api/tier/start-trial` | → `{ tier, rawTier, trialStartedAt, trialDaysRemaining, trialExpired, capabilities }` (409 if already started) |
| POST | `/api/stripe/create-checkout-session` | `{ tier:'PRO'\|'TEAMS', billingPeriod?:'monthly'\|'annual' }``{ url }` (503 `STRIPE_NOT_CONFIGURED` if unset) |
| POST | `/api/stripe/sync` | `{ sessionId }``{ tier, customerId }`**call this after the checkout redirect** (webhooks unreliable behind NAT) |
| POST | `/api/stripe/create-portal-session` | — → `{ url }` (requires PRO+) |
**Stripe flow for the rebuild:** open `{url}` from create-checkout-session → user pays → Stripe redirects to
`/payment-success?session_id=...``useBilling` detects `?session_id=` on load → `POST /api/stripe/sync`
refresh tier. Render upgrade UI defensively when `503 STRIPE_NOT_CONFIGURED`.
---
## 5. Design system — Hive DS (dark default, desktop-OS metaphor)
The visual contract lives in `apps/web/src/index.css` (`@layer base` source of truth) + `waggle-theme.css`
(component aliases). It's a **shadcn-style HSL-variable system + a literal Hive palette**, with a `[data-theme="light"]`
override. **Default theme is dark.** Tailwind 4 + shadcn primitives + lucide icons.
### 5.1 Core brand tokens (the three the brief names)
| Token | Dark value | Role |
|---|---|---|
| Honey `--honey-500` | `#e5a000` | Primary brand / accent / focus ring / `--primary` (`40 100% 45%`) |
| Hive-950 `--hive-950` | `#08090c` | Deepest background / status bar; `--background``222 20% 4%` |
| Accent (AI) `--status-ai` / `--accent` | `#a78bfa` / `270 60% 68%` | Secondary accent (AI, knowledge-concept highlights) |
### 5.2 Full palette (use the CSS variables, never hard-code hex)
- **Hive grays** (cold undertone): `--hive-950 #08090c → --hive-50 #f0f2f7` (12 steps). Surfaces:
`--surface-card: var(--hive-850)`, `--surface-panel: var(--hive-800)`, `--surface-overlay: rgba(8,9,12,0.88)`.
- **Honey scale:** `--honey-600 #b87a00 … --honey-50 #fffbeb`, plus `--honey-glow rgba(229,160,0,0.12)`,
`--honey-pulse rgba(229,160,0,0.06)`.
- **Status:** `--status-healthy #34d399`, `--status-warning #fbbf24`, `--status-error #f87171`,
`--status-info #60a5fa`, `--status-ai #a78bfa`.
- **Knowledge-graph nodes:** `--kg-person #4A90D9`, `--kg-project #50C878`, `--kg-concept #9B59B6`,
`--kg-org #E67E22`, `--kg-default #95A5A6`.
- **shadcn semantic vars** (HSL triplets, consumed via `hsl(var(--x))`): `--background, --foreground, --card,
--popover, --primary (40 100% 45%), --secondary, --muted, --accent (270 60% 68%), --destructive, --border,
--input, --ring (40 100% 45%), --radius 0.75rem`. Sidebar + chart vars mirror these.
- **Step/event colors** (event stream): `--step-running/-success/-pending/-error/-thinking/-search/-web/-tool/-writing`.
### 5.3 Typography, radius, shadows, motion
- **Fonts:** headings `Space Grotesk`; body `DM Sans` (fallback Inter/system); mono `JetBrains Mono`. Imported
from Google Fonts in `index.css`. `--font-sans`, `--font-mono` aliases exist.
- **Type scale:** `--text-micro 11 → --text-display 24` (micro 11, caption 12, body-sm 13, body 14, title 16,
heading 20, display 24).
- **Radius:** `--radius: 0.75rem`.
- **Shadows:** `--shadow-card`, `--shadow-elevated`, `--shadow-overlay`, `--shadow-honey` (honey glow),
`--shadow-focus` (2px honey ring).
- **Signature motifs:** glassmorphism (`.glass` / `.glass-strong` — backdrop blur 2030px), honeycomb hex
background (`.honeycomb-bg`, SVG data-URI at 3% honey opacity), hex avatar clip-path (`.hex-avatar`),
hex streaming cursor (`.hex-cursor`), honey-pulse on memory-save, heartbeat on health dot, float on the bee
mascot, token-fade on streamed text. Selection + thin 5px scrollbars are honey/hive themed.
- **Interaction utilities:** `.waggle-interactive`, `.waggle-card-lift`, `.waggle-nav-hover`, `.waggle-press`,
`.direction-d-card` (the canonical card: hive-700 border → honey-500 + honey-shadow on hover).
### 5.4 Light mode
`:root[data-theme="light"]` flips the hive scale (cream `#fdfcf9` bg, dark text), darkens honey + status +
KG colors for WCAG AA on cream, and softens shadows. Toggle by setting `data-theme="light"` on `:root`.
**Ship dark first**; light is a polish pass.
### 5.5 Layout — the desktop OS metaphor
- **Desktop**: full-viewport, `overflow:hidden` body, honeycomb wallpaper + `.desktop-overlay` wash.
- **Dock**: launcher rail (bottom/side) built from `lib/dock-tiers.ts`. Entries are `app | zone-parent
(collapsible group) | separator`, each with `icon` (lucide), `label`, `color`. `getDockForTier(userTier,
billingTier)` filters by `minBillingTier` (e.g. `governance` & `approvals` are TEAMS+; empty zone-parents are
dropped). `simple` tier shows ~6 apps; `power`/`admin` show the full set incl. Ops + Extend zone groups.
- **Windows**: each open app is a draggable/resizable `AppWindow` keyed by `appId`; content via a
`renderAppContent(appId)` switch. Windows carry per-window persona + autonomy.
- **Overlays**: modals/rails/wizards rendered directly by Desktop, gated by `useOverlayState` flags.
- **BootScreen**: shown until `localStorage["waggle-booted"]`, then Desktop mounts.
---
## 6. Prioritized rebuild order
Build in dependency order. Each phase is independently demoable.
**Phase 0 — Foundation (nothing renders without this).**
1. `adapter` singleton: base URL + `localStorage["waggle:server-url"]`, `connect()` (health probe + session-token
bootstrap), the `fetch` wrapper (conditional content-type, Bearer, 403→`waggle:tier-insufficient`,
10s/30s timeouts, 429 handling), and the §1.4 read normalizers.
2. `ServiceProvider` / `useService()` calling `adapter.connect()` once.
3. Hive DS tokens (`index.css` + `waggle-theme.css`), dark default, fonts, shadcn vars.
4. The `window` event bus (§3.4).
**Phase 1 — Shell.**
5. Desktop shell: `useWindowManager` (open by `appId`), Dock from `dock-tiers.ts`, `AppWindow` + `renderAppContent`
switch, BootScreen, `useOverlayState`, `useKeyboardShortcuts` (Cmd+K).
6. `useWorkspaces` (active workspace context) + Workspace switcher + Create-workspace dialog.
7. `useBilling` + tier gating (`getEffectiveTier` → capabilities → `useFeatureGate`) + Upgrade/TrialExpired modals.
**Phase 2 — The product's core loop (Chat).**
8. **Chat** (`chat`) end-to-end: `POST /api/chat` fetch-stream parser → `ContentBlock[]`, all SSE events,
inline approvals (`POST /api/approval/:id`), history, model switch, pins, feedback. This is the single
highest-value screen — do it first and well.
9. **Dashboard / Home** (`dashboard`) — cheap, gives a landing surface (`/api/memory/stats`, `/api/tasks`).
**Phase 3 — Memory moat (the strategic lock-in).**
10. **Memory** (`memory`): Frames + Knowledge Graph tabs first, then **Harvest** (import is the moat: scan +
preview/commit + progress SSE), then Wiki, then Evolution.
11. **Files** (`files`) — workspace file CRUD + upload/download.
**Phase 4 — Real-time ops + agents.**
12. **Events** (`events`, SSE), **Room** (`room`, `subagent_status` SSE), **Waggle Dance** (`waggle-dance`, SSE).
13. **Personas/Agents** (`agents`) + Spawn agent dialog + Mission Control (`mission-control`, fleet).
14. **Scheduled Jobs** (`scheduled-jobs`), **Notifications** inbox.
**Phase 5 — Extensibility + monetization surfaces.**
15. **Skills & Apps** (`capabilities`) + **Marketplace** (`marketplace`) + **Connectors** (`connectors`) +
**Vault** (`vault`) — these are the upgrade triggers; wire 403→Upgrade carefully.
16. **Settings** (`settings`), **My Profile** (`profile`), **Cockpit** (`cockpit`) + Compliance.
**Phase 6 — Governance / admin / polish.**
17. **Approvals** (`approvals`, TEAMS+), **Team Governance** (`governance`, TEAMS+), **Timeline** (`timeline`),
**Telemetry** (`telemetry`), **Backup** (`backup`), **Erase data** (GDPR).
18. **Onboarding wizard** (8 steps) + Login briefing + Global search polish.
19. **Light mode** pass.
**Rationale:** Phases 01 are non-negotiable scaffolding. Chat (Phase 2) is the product. Memory/Harvest (Phase 3)
is the strategic moat ("free forever" lock-in). Real-time + agents (Phase 4) prove the "OS" thesis.
Monetization surfaces (Phase 5) are where tier gating earns money. Governance + polish (Phase 6) come last.
---
## 7. Gotchas checklist (the things that bite a rebuild)
- [ ] Chat is **POST-SSE**, not `EventSource` — parse the body stream manually.
- [ ] Body-less POSTs must **not** send `Content-Type: application/json`.
- [ ] `EventSource` can't send Bearer headers — the SSE streams are **same-origin gated**; proxy or polyfill in dev.
- [ ] Run tier through **`getEffectiveTier`** before gating (expired TRIAL → FREE).
- [ ] `-1` in any capability/limit means **unlimited**, not "zero/disabled".
- [ ] Reproduce the §1.4 read normalizers or the UI breaks on renamed fields.
- [ ] Echo back `sourceWorkspaceId` verbatim when resolving approvals.
- [ ] After Stripe redirect, call `POST /api/stripe/sync { sessionId }` — do not trust the webhook for desktop.
- [ ] Handle two `done` usage shapes (agent-loop vs echo/command path).
- [ ] `/api/notifications/stream`: `notification` is an **unnamed** frame (`onmessage`); the others are named events.
- [ ] All `/api/*` paths are **flat** — no plugin prefix; the literal path in the table IS the path.
- [ ] Respect rate limits (chat 120/min, vault reveal 5/min, backup/restore 2/min) — handle `429` + `Retry-After`.