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

36 KiB
Raw Permalink Blame History

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/Ninsight/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 tooltool_start)
    tool_result { name, result, duration?, isError } close tool block (normalized tool_resulttool_end)
    file_created `{ filePath, fileAction: 'write' 'edit'
    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 "
    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', …)`
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.

// 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 contextuseWorkspaces.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. TieruseBilling.tiergetEffectiveTier → 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) Desktopwm.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; --background222 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.