Summary
Migrate the web UI from vanilla ES modules (web/static/js/) to a typed React frontend while keeping the existing backend architecture :
Layer
Decision
Shell
tao + wry (openpolysphere-app) — no change, no Tauri
Frontend
Vite+ + React 19 + TypeScript (strict) + Zod or ArkType
API
Axum REST + SSE on 127.0.0.1:5050 — no change
Types
utoipa → OpenAPI → openapi-typescript (+ Zod/ArkType runtime parse)
DB
rusqlite WAL (calls.db, in-process) — no change
Package manager
Bun via Vite+ (vp install, vp check, vp build)
The desktop shell continues to spawn translator serve as a sidecar and load the UI over localhost — same as just app today. Browser dev (just run) stays supported.
Problem
Frontend is ~35 vanilla JS modules with no static types; API contracts live only in routes.rs and ad-hoc fetch() calls.
ESLint-only JS linting; no typecheck, no HMR, no component model.
Harder to scale UI work (29 locales, settings, call history, audio controls) without regressions.
Proposed solution
Architecture (unchanged IPC boundary)
openpolysphere-app (tao + wry)
└── React UI (Vite+ build → web/static or embedded assets)
│ typed REST + SSE
▼
translator (Axum sidecar)
├── audio-core (STT / translation / TTS)
└── rusqlite WAL (call history)
Frontend toolchain
Vite+ (vp migrate, vp dev, vp check, vp build) — unified lint (Oxlint), format (Oxfmt), typecheck (tsgo), and bundling.
React 19 with strict TypeScript (noUncheckedIndexedAccess, exactOptionalPropertyTypes, verbatimModuleSyntax, etc.).
Runtime validation at the HTTP boundary: Zod 4 (ecosystem) or ArkType (speed) on fetch responses.
Feature-oriented structure mirroring current JS modules (features/translator, features/settings, features/audio-controls, features/call-history, …).
API typing (Rust → TypeScript)
Add utoipa to translator to generate OpenAPI from existing Axum routes.
Generate TS client types with openapi-typescript or orval .
Parse responses with generated Zod/ArkType schemas — Rust remains the source of truth (serde + validator on inbound POST bodies).
What stays the same
translator binary, SSE /stream, POST /cmd, all /api/* routes.
crates/audio-core, ONNX models, Swift helpers (macOS).
openpolysphere-app shell and macOS/Linux/Windows packaging scripts.
SQLite call history — in-process is faster than Postgres for a single-user desktop app.
Phases
1. Toolchain bootstrap
2. OpenAPI + typed client
3. UI migration (feature by feature)
features/translator — transcript, bubbles, latency metrics, SSE hook.
features/audio-controls — start/stop, mute, monitor, levels.
features/settings — keys, STT, translation, voices, devices, theme.
features/call-history — port history.html + /api/calls/*.
i18n — migrate 29 web/static/locales/*.json (typed keys).
Remove vanilla JS modules once parity is reached.
4. CI / docs
Acceptance criteria
vp check passes in CI (lint + format + typecheck).
All existing UI features work in browser (just run) and desktop shell (just app).
API calls are fully typed end-to-end; no any on fetch responses.
SSE realtime stream unchanged (/stream).
User data, models, and calls.db paths unchanged after migration.
Bundle size and production build tracked (no regression vs current static assets).
Out of scope (explicitly rejected for this track)
Alternative
Reason
Tauri
Desktop shell already ships via tao + wry + sidecar; Tauri adds complexity without improving audio/STT path. Updater tracked separately in #37 .
GraphQL
Hot path is high-frequency SSE text stream, not nested queries; GraphQL subscriptions are heavier with no benefit for ~24 REST routes.
PostgreSQL
Local in-process SQLite + WAL is faster than TCP Postgres for single-user desktop; Postgres only if cloud sync/multi-user is added later.
Tauri IPC / drop Axum
Axum-first keeps one binary for cargo run, CI, browser dev, and packaged app; localhost HTTP overhead is negligible.
Full rewrite of audio-core layout
Engine stays in existing crates.
Related
Current shell: crates/openpolysphere-app/
Routes / SSE: crates/translator/src/routes.rs, events.rs
Frontend (to migrate): web/static/js/
DB: crates/translator/src/db.rs
Dev tasks: Justfile
Prior Tauri notes (superseded by this decision): docs/tauri-next.md
Help wanted: phase 1 (toolchain + one typed route) is a good first PR; UI migration can proceed feature-by-feature.
Summary
Migrate the web UI from vanilla ES modules (
web/static/js/) to a typed React frontend while keeping the existing backend architecture:tao+wry(openpolysphere-app) — no change, no Tauri127.0.0.1:5050— no changeutoipa→ OpenAPI →openapi-typescript(+ Zod/ArkType runtime parse)rusqliteWAL (calls.db, in-process) — no changevp install,vp check,vp build)The desktop shell continues to spawn
translator serveas a sidecar and load the UI over localhost — same asjust apptoday. Browser dev (just run) stays supported.Problem
routes.rsand ad-hocfetch()calls.Proposed solution
Architecture (unchanged IPC boundary)
Frontend toolchain
vp migrate,vp dev,vp check,vp build) — unified lint (Oxlint), format (Oxfmt), typecheck (tsgo), and bundling.noUncheckedIndexedAccess,exactOptionalPropertyTypes,verbatimModuleSyntax, etc.).fetchresponses.features/translator,features/settings,features/audio-controls,features/call-history, …).API typing (Rust → TypeScript)
utoipatotranslatorto generate OpenAPI from existing Axum routes.openapi-typescriptororval.serde+validatoron inbound POST bodies).What stays the same
translatorbinary, SSE/stream,POST /cmd, all/api/*routes.crates/audio-core, ONNX models, Swift helpers (macOS).openpolysphere-appshell and macOS/Linux/Windows packaging scripts.Phases
1. Toolchain bootstrap
vp migrate— addui/workspace (or top-level frontend package) with React 19 + stricttsconfig.json.vp checkintojust check(replace ESLint for new frontend; keep ESLint until vanilla JS is removed).vite buildoutput →web/static/(or path consumed bytranslatorServeDir).:5050, orjust run+vp dev.2. OpenAPI + typed client
utoipaon core routes:/api/settings,/api/calls,/api/devices,/api/voices,/health.apiClientwith Zod/ArkType parse on every response.3. UI migration (feature by feature)
features/translator— transcript, bubbles, latency metrics, SSE hook.features/audio-controls— start/stop, mute, monitor, levels.features/settings— keys, STT, translation, voices, devices, theme.features/call-history— porthistory.html+/api/calls/*.web/static/locales/*.json(typed keys).4. CI / docs
just install,just check,just prepushfor Vite+.Acceptance criteria
vp checkpasses in CI (lint + format + typecheck).just run) and desktop shell (just app).anyon fetch responses./stream).calls.dbpaths unchanged after migration.Out of scope (explicitly rejected for this track)
tao+wry+ sidecar; Tauri adds complexity without improving audio/STT path. Updater tracked separately in #37.cargo run, CI, browser dev, and packaged app; localhost HTTP overhead is negligible.audio-corelayoutRelated
crates/openpolysphere-app/crates/translator/src/routes.rs,events.rsweb/static/js/crates/translator/src/db.rsJustfiledocs/tauri-next.mdHelp wanted: phase 1 (toolchain + one typed route) is a good first PR; UI migration can proceed feature-by-feature.