# Zyquo Cloud Web — Build Plan & Phase Log Strict phase order 0 → 8. Each phase gets a checklist here, checked off as work lands, plus a 3–5 line summary at its checkpoint. --- ## Phase 0 — Research + Zyquo Cloud study - [x] Locate native Zyquo Cloud repo (`~/Desktop/zyquo-cloud`) - [x] Study provider layer (ProviderID, ProviderRegistry, OpenAICompatibleClient, AnthropicClient, StreamingService/SSEParser, ProviderProtocol) - [x] Study AIModel / capabilities / pricing / ParameterSupport model - [x] Study ZyquoTheme design tokens - [x] Extract complete model catalog (170 models, 12 providers — port from ModelCatalogData.swift, NOT the drifted catalog-summary.md) - [x] Study native UX (views, model picker, personas, prompt library, brand glyph) → `docs/NATIVE-UX.md` - [x] Empirical CORS probes, all 12 providers (preflight + real POST with Origin) - [x] Anthropic browser header confirmed (`anthropic-dangerous-direct-browser-access: true`) - [x] Web research: documented CORS stance per provider (cited in CORS-MATRIX) - [x] Write `docs/CORS-MATRIX.md` - [x] Write `docs/PROVIDER-REUSE.md` **Phase 0 summary.** Native repo studied end-to-end: only two wire formats (OpenAI chat/completions ×11 incl. Gemini-compat; Anthropic Messages ×1), one SSE parser, 170-model catalog, ZyquoTheme tokens extracted. Empirical CORS probes with real keys: **all 12 providers callable directly from the browser** (Anthropic needs `anthropic-dangerous-direct-browser-access: true`); documented stances researched and cited. Proxy/Zyquo Router override still ships since only Anthropic documents CORS officially. Gate satisfied: PROVIDER-REUSE.md + CORS-MATRIX.md complete. --- ## Phase 1 — Project setup (React + TS + Vite) - [x] Vite + React 19 + TypeScript strict (project references, `noUncheckedIndexedAccess`) - [x] Styling: CSS variables implementing ZyquoTheme (`src/design/tokens.css` + `global.css`) — no Tailwind (dependency minimalism = smaller XSS surface) - [x] State: Zustand installed - [x] ESLint (flat config) + Prettier; scripts `dev/build/preview/lint/typecheck/check` - [x] PWA scaffold (vite-plugin-pwa): app-shell precache only, NO API/key caching, manifest (light theme-color) - [x] Strict CSP meta in `index.html`: `connect-src` = exact 12 provider origins + localhost (Zyquo Router); no inline/third-party scripts - [x] Phase checkpoint: `npm run check` green (typecheck, lint, build) **Phase 1 summary.** Hand-scaffolded Vite/React/TS-strict app with mandatory headers everywhere, ZyquoTheme tokens as CSS variables (light default + dark + 5 accents), a locked-down CSP whose connect-src is exactly the 12 provider origins plus localhost for Zyquo Router, and an app-shell-only PWA. Build, lint, and typecheck all green on first checkpoint. ## Phase 2 — Architecture + browser provider layer - [x] `src/types/` — Provider, WireFormat, AIModel, capabilities/pricing/ParameterSupport, TokenUsage, Citation, Attachment, Message (+variants), Conversation, Settings - [x] `providers/types.ts` — ProviderClient interface, ChatRequest/ChatEvent, ProviderError (typed, human messages, CORS-shaped network hint) - [x] `providers/registry.ts` — 12-provider metadata (base URLs, auth, wire format, stream_options wants, /models support) + clientFor() - [x] `providers/sse.ts` — SSEParser state machine + fetch/ReadableStream event stream + retrying requestJSON (429/5xx backoff, Retry-After) + AbortController cancellation - [x] `providers/openaiCompatible.ts` — all native quirks ported (param gating, Mistral effort mapping + array-content reasoning, Qwen enable_thinking, Together choices[].text, Perplexity citations, Gemini models/ prefix, requiresStreaming aggregation, vision parts, text-file fencing) - [x] `providers/anthropic.ts` — Messages API (mandatory max_tokens, block content, named SSE events, thinking toggle, browser CORS header) - [x] `providers/catalog.ts` — ALL 170 models transcribed 1:1 (counts verified: 27/11/5/10/14/32/2/12/4/16/34/3) + rank/cheapest/default/find helpers - [x] `storage/keys.ts` — namespaced localStorage key map (gate prerequisite) - [x] PHASE GATE: real token-by-token streaming in a real Chromium browser via `#stream-test` — 26 incremental snapshots, usage `15→256 tok`, `finished (length)`, Stop → `cancelled: Generation stopped.` (scripts/gate-phase2.mjs) **Phase 2 summary.** The whole native provider layer now runs on browser fetch + ReadableStream: one OpenAI-compatible client (11 providers + custom), one Anthropic Messages client, one SSE parser for both stream shapes, and the complete 170-model catalog with per-model parameter gating. Gate passed against live OpenAI from headless Chromium: token-by-token rendering, usage events, and AbortController cancellation all verified. Playwright added (dev-only) as the browser harness that Phase 7 will reuse for the full matrix. ## Phase 3 — Local persistence - [x] 3.A `storage/keys.ts` — namespaced localStorage map, get/set/remove, masked display, in-memory overlay for vault mode; `providers/testKey.ts` Test action (/models or min completion, ms latency) - [x] 3.A `storage/vault.ts` — opt-in passphrase lock: PBKDF2 (310k, SHA-256) → AES-GCM; enable/unlock/lock/disable; honest framing (obfuscation-at-rest, not a secure vault) - [x] 3.B `storage/db.ts` (dependency-free IndexedDB promise wrapper) + `storage/conversations.ts` — CRUD, updatedAt sorting, schema versioning + migrate(), title+message search - [x] 3.C `storage/settings.ts` — theme (light default), default model, per-provider proxy URLs, UI prefs - [x] 3.C `storage/backup.ts` — export (keys opt-in with warning) / import / clear-everything - [x] Dev harness (`features/devHarness.ts`, DEV-only) exposing storage for browser gate scripts - [x] PHASE GATE: 13/13 checks in real Chromium (scripts/gate-phase3.mjs) — reload survival, **browser-restart survival** (persistent context), export→clear→import exact restore, vault encrypt-at-rest / wrong-pass rejection / unlock / disable **Phase 3 summary.** All state is now local and durable: keys in `zyquo.cloud.web.keys`, conversations in IndexedDB with schema versioning, settings in `zyquo.cloud.web.settings`. Export/import is the backup story; clear-data wipes everything. The optional passphrase lock encrypts the key map with WebCrypto AES-GCM and is honestly framed. Gate ran in a real browser with a persistent profile: reload AND restart survival, exact export round-trip, and vault behavior all green. ## Phase 4 — Design system & UI - [x] 4.1 Light flagship theme via tokens (Phase 1 CSS vars) — light default, dark opt-in, 5 accents, reduced-motion respected - [x] 4.2 Shell: sidebar (wordmark, search ⌘F, New Chat ⌘N, Pinned/Today/Yesterday/7 Days/Older groups, hover pin/delete, usage footer) + chat area (header 52px with editable title, centered model chip, compare/export/info actions) - [x] 4.2.1 Model Menu: ⌘K command-palette style — fuzzy search, capability filter chips (Vision/Tools/Reasoning/JSON/Long context/Cheapest), Aliases + Favorites + Recents groups, keyed-providers-first sections, capability/context/price badges, star favorites, "Add key" affordance, keyboard nav (↑↓/↵/⌘↵ default/esc), footer hints - [x] Transcript: centered 760px column, user right accent-subtle / assistant left surface+avatar, GFM markdown + tables + zebra rows + syntax-highlighted code (language label, copy, wrap), collapsible reasoning, citation chips, streaming caret, scroll-lock + jump-to-latest pill, hover metadata (time, tokens, cost) + actions (copy/edit-resend/regenerate/quote/branch/read-aloud/delete), variant switcher - [x] Input bar: floating card, auto-grow, image+text-file attach (click/drag/paste), thumbnails, params popover (support-gated controls with enable checkboxes), circular send ⌘↩ / Stop, context-usage bar + token/cost HUD - [x] Settings modal: Providers & Keys (masked, Test + latency + status dot, per-provider proxy URL, honesty notice), Models (catalog browser, favorites, set-default), Appearance (theme/accent/font-size/density + live preview), Data (export incl.-keys warning / import / confirm-twice clear), Advanced (default system prompt, passphrase lock, streaming toggle) - [x] First-run honest welcome + root empty state (glyph, greeting, model chip, 4 suggestion cards — self-contained ones send immediately, input-expecting ones seed the draft) - [x] Toasts with undo (conversation delete) - [x] 4.3 Responsive: sidebar drawer <768px with scrim, full-width transcript, code blocks scroll internally, no horizontal overflow; motion tokens (80ms hover, 150ms message-in, blinking caret) - [x] 4.4 Quality gate: screenshotted desktop+phone, light+dark, model menu, settings — reviewed and fixed (filter-chip clipping, cost precision, draft seeding) **Phase 4 summary.** The full chat UI is live and matches the family design: tokens-only styling, light flagship default, dark derived, responsive down to phone width with a drawer sidebar. The Model Menu is the headline surface — fuzzy-searchable, filterable, priced, favorited, keyboard-driven. Verified visually in headless Chromium at 1240px and 390px in both themes; found and fixed three issues in the review pass. `npm run check` green. ## Phase 5 — Brand assets - [x] App icon reused from native (`assets/icon/zyquo-cloud.svg` — same squircle/gradient/Z-caret) → rasterized `icon-192/512.png`, `icon-512-maskable.png` (80% safe zone on brand gradient), `apple-touch-icon.png` - [x] `favicon.svg` (native small mark); manifest icons wired in vite.config PWA manifest; `theme-color` light `#FAFBFD` - [x] Wordmark: crisp inline `ZyquoGlyph` SVG component (currentColor, light/dark via tokens) - [x] OG/social meta + generated 1200×630 `og-image.png` (light canvas, icon + wordmark + tagline + domain) **Phase 5 summary.** The web app now carries the exact native Zyquo Cloud icon (Big Sur squircle, sky gradient, white Z with block caret) as its PWA identity, matching the Dock/home-screen look of the macOS app. Favicon, apple-touch, maskable, OG image and social meta are all in place. ## Phase 6 — Features - [x] Personas: all 8 native personas ported verbatim (`features/personasData.ts`); apply via palette or slash menu (system prompt + personaID per conversation) - [x] Prompt Library: all 56 native templates ported verbatim (8 categories × 7, `features/promptLibraryData.ts`), `{{input}}` substitution - [x] Slash commands: `/` in the input → menu with quick tools (/summarize /translate /rewrite /explain), full template library, personas; keyboard-driven - [x] Command palette (⌘K): actions (new chat, compare, switch model, settings, focus mode, summarize, duplicate), jump-to-conversation, models, personas, templates - [x] Multi-model compare: 2–4 columns, per-column model chip/stream/stop/copy/regenerate, broadcast prompt, **promote answer into thread** - [x] Branching/variants (Phase 4 store): regenerate keeps prior answer as a variant with ‹ › switcher; branch-from-message forks a conversation; edit & fork - [x] Rich rendering: GFM + tables + task lists (Phase 4) + **KaTeX math** + **Mermaid diagrams** (lazy-loaded ~1MB chunk, strict security level) - [x] Read-aloud (Web Speech TTS) + dictation (SpeechRecognition, mic button) - [x] Continue generation (last assistant message action), Stop, live elapsed + tok/s - [x] Token/cost HUD + context bar (Phase 4) + **auto-trim oldest turns** at 90% of the window (system prompt kept, toast informs) - [x] Conversation tools: summarize (cheapest model), duplicate, export MD/JSON, undo delete, pin, rename, search - [x] QoL: retry w/ backoff (Phase 2), human error toasts naming the provider + CORS hint, autosave everywhere, PWA shell precache trimmed 4.7MB → 1.15MB (mermaid/katex runtime-cached) - [x] Coherence sweep #2: headers ✓, storage/network layering ✓, accent hexes moved to design/accents.ts **Phase 6 summary.** Full feature parity plus the web-specific extras: the native personas and 56-template library are ported verbatim and surfaced through both the ⌘K palette and a `/` slash menu; compare mode broadcasts to up to four independently streaming columns with promote-to-thread; math and diagrams render (lazily); reading aloud, dictation, auto-trim, and conversation tools round out the spec. Sweep clean, `npm run check` green. ## Phase 7 — Verification - [x] Provider matrix with real keys from a real browser: **169/169 tested models PASS streaming** (sonar-deep-research skipped by design); 12/12 non-streaming; 12/12 Stop/abort; vision on all 9 vision-capable providers; Perplexity citations (19); reasoning deltas on all thinking models → `docs/VERIFICATION.md` - [x] Persistence: Phase 3 gate 13/13 (reload + browser restart + export/import + vault) - [x] Security/privacy: network audit = 12 provider origins + localhost only; CSP blocks foreign origins; first-run notice; no dev harness in prod build; keys never committed - [x] Cross-browser/responsive: 15/15 on Chromium + WebKit + Firefox, desktop + phone, light default **Phase 7 summary.** The whole catalog was exercised live from a browser context using the user's keys: every one of the 169 tested models streamed token-by-token through the ported provider layer, with non-streaming, cancellation, vision, reasoning, and citations verified per provider, and the network audit confirming not a single byte leaves the page except toward the chosen providers. Three first-pass failures were all test-harness artifacts (tiny token budgets on reasoning models, an abort timer slower than Cerebras) and retested green. ## Phase 8 — Build & deployment - [x] Production build: `npm run check` green (0 type/lint errors), code-split (mermaid/katex lazy), hashed assets, PWA precache trimmed to app shell (1.15 MB) - [x] Zero-dependency production server (`scripts/server.mjs`): CSP (exact 12 provider origins + localhost), HSTS preload, nosniff, Referrer-Policy no-referrer, Permissions-Policy, immutable cache for hashed assets / no-cache HTML, SPA fallback, `/healthz` - [x] Deployed on cluster node **M3U96a** (per user instruction; M2U64 stays prod-reserved): rsync → `~/zyquo-cloud-web`, PM2 `zyquo-cloud-web` (node server.mjs 8080 dist) + `zyquo-cloud-ngrok` (`ngrok http --url=www.zyquo.cloud 8080`), `pm2 save` - [x] Registered in `~/Desktop/cluster-skill/cluster-deployments.json` - [x] Post-deploy verification: **https://www.zyquo.cloud** serves HTTP/2 200 with all security headers; manifest + service worker registered (PWA installable); **light theme default**; first-run notice; **real streaming completion from the deployed origin** (CSP allows providers) — `finished`, token-by-token - [x] Self-hosting documented in README (static bundle + server.mjs or any static host; Zyquo Router pairing) - [x] Final sweeps: every code file carries the Author/Mail header; storage/network layering intact - [ ] ⚠ Apex `zyquo.cloud` → `www` redirect: the apex currently has **no DNS record** — needs a registrar-level ALIAS/forward to `www.zyquo.cloud` (requires registrar access; the only item outside this environment's reach) **Phase 8 summary.** Zyquo Cloud Web is live at **https://www.zyquo.cloud**, served as static assets from cluster node M3U96a through the user's ngrok domain, under PM2 with the process list saved. Security headers verified on the wire, the PWA installs, light loads by default, and a live provider stream was rendered from the deployed origin. The single open item is the apex DNS record (registrar access required).