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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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