Skip to content

feat: modern frontend stack — Vite+, React 19, strict TS, OpenAPI types (Axum-first) #47

Description

@org-event

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

  • vp migrate — add ui/ workspace (or top-level frontend package) with React 19 + strict tsconfig.json.
  • Wire vp check into just check (replace ESLint for new frontend; keep ESLint until vanilla JS is removed).
  • vite build output → web/static/ (or path consumed by translator ServeDir).
  • Dev workflow: Vite dev server with proxy to :5050, or just run + vp dev.

2. OpenAPI + typed client

  • utoipa on core routes: /api/settings, /api/calls, /api/devices, /api/voices, /health.
  • CI step: regenerate OpenAPI spec + TS types; fail on drift.
  • Shared apiClient with 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 — 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

  • Update just install, just check, just prepush for Vite+.
  • Update README dev section (no more “ESLint only”).
  • ADR documenting stack decision and rejected alternatives.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions