# Leptos Rewrite — Frontend Architecture Design **Date:** 2026-07-03 **Status:** Approved Design ## Overview Rewrite the React 19 + Vite + Tailwind dashboard (46 source files, ~6k LOC) to a Rust Leptos CSR WASM app. All layers — components, styling, animations, WebSocket, audio, icons — are rewritten in Rust. ## Motivation - **Performance** — WASM eliminates JS bundle parsing/execution overhead - **Type Safety** — Rust's type system catches more errors at compile time - **Bundle Size** — WASM binary smaller than equivalent JS bundle ## Status The current React frontend (`services/frontend/`) remains in active use. The new Leptos app lives in `services/frontend-leptos/`. No React code is removed until the Leptos version is feature-complete. ## Tech Stack | Layer | Choice | Rationale | |-------|--------|-----------| | Framework | Leptos 0.7 (CSR) | WASM-native reactive framework, signals-based | | Styling | Plain CSS | No framework dependency, port from existing CSS | | Animations | CSS keyframes + transitions | Replace all Framer Motion usage | | Icons | `lucide-leptos` | Direct port of current lucide-react icons | | HTTP | `gloo-net` | Rust fetch wrapper, works with WASM | | WS | `web-sys::WebSocket` | Direct binding, no abstraction overhead | | Audio Viz | Canvas 2D (`web-sys`) | AudioVisualizer, WaveformPlayer | | Audio Playback | Web Audio API (`web-sys`) | PCM stream playback | | State | Leptos signals + contexts | No external state library needed | | Build | `trunk` | Standard WASM builder for Leptos | ## Architecture ``` Leptos App (WASM) │ ├── App Shell (layout, sidebar, header, tabs) ├── Messages Panel (feed, cards, search, filters) ├── Live Panel (voice, music, screen, recordings, audio viz) └── Dashboard Panel (stats, users, channels) Shared infrastructure: ├── WebSocket Context (singleton + per-event channels) ├── API Client (fetch wrapper + typed endpoint functions) ├── UI Primitives (Button, Card, Badge, Input, Tabs, Toast, etc.) └── CSS Design System (custom properties, component classes, animations) ``` ### Workspace Structure ``` services/frontend-leptos/ ├── Cargo.toml # workspace root ├── shared-types/ # Rust types (port of @bete/shared) │ ├── Cargo.toml │ └── src/ │ ├── lib.rs │ ├── message.rs # MessageRecord, AttachmentRecord │ ├── guild.rs # Guild, Channel │ ├── voice.rs # VoiceStatus, ActiveSpeaker │ ├── media.rs # MediaItem, MediaState, MediaMode │ ├── dashboard.rs # DashboardStats, DashboardUser, DashboardChannel │ ├── recording.rs # VoiceRecording │ └── ui_state.rs # UIState, AppConfig ├── frontend/ │ ├── Cargo.toml # Leptos + deps │ ├── Trunk.toml # Build config │ ├── index.html # Entry point (lang="id", Poppins font) │ └── src/ │ ├── main.rs # mount_to_body with panic_hook + logger │ ├── app.rs # Root component (providers + auth gate + routing) │ ├── app.css # Full CSS design system │ ├── lib.rs # Module declarations │ ├── auth.rs # AuthOverlay + login logic │ ├── ws/ # WebSocket context & event system │ ├── api/ # HTTP client + typed API functions │ ├── ui/ # Shared UI primitives │ ├── layout/ # App shell components │ └── features/ │ ├── messages/ # Message feed, cards, search, filters │ ├── live/ # Voice, audio, media, recordings │ └── dashboard/ # Stats, users, channels ├── rust-toolchain.toml # nightly (required by Leptos CSR) └── .env # API_URL, WS_URL ``` ### Component Tree ``` {!authenticated && !is_public → } Messages | Live | Dashboard
{active_tab == "messages" → } {active_tab == "live" → } {active_tab == "dashboard" → }
``` ### State Architecture **Contexts** (provided at `` level via `provide_context`): | Context | Key Signals | Persisted | |---------|-------------|-----------| | AuthContext | `authenticated: ReadSignal` | sessionStorage | | WsContext | `status: ReadSignal`, per-event channels | — | | UiContext | `active_tab: RwSignal`, `selected_guild: RwSignal>`, etc. | localStorage | **Data fetching** via Leptos `Resource` (parallel to React useEffect + fetch, with built-in loading/error/ok states). **WebSocket events** dispatched through per-event-type `mpsc::UnboundedReceiver` channels. Components subscribe to relevant channels via `watch!` or `create_effect`. ## State Machine: Tab Navigation ``` [messages] ←── [live] ←──→ [dashboard] | ↑ └─── (unauth redirect) ──┘ ``` - Default: "messages" - Unauthenticated on "live" → redirect to "messages" (same as current behavior) - Tab state persisted via localStorage ## Data Flow ### Message Capture → Display ``` Discord → gateway → Redis → backend → WS → ws::socket receive → parse JSON → match event type → WsContext.on_message_created.send(data) → watch! { set_messages.update(|msgs| merge(msgs, data)) } → UI re-renders via Leptos reactivity ``` ### Voice PCM → Audio Playback ``` Discord → gateway → Redis → backend → WS binary frame → ws::socket onbinary → parse [u32be userId][i16 samples] → WsContext.on_voice_pcm.send(packet) → use_audio_playback → AudioContext.decodeAudioData → play via AudioBufferSourceNode → AudioVisualizer: CanvasRenderingContext2D draw 32 bars on requestAnimationFrame ``` ### User Action → Command ``` Button click → call api::voice_connect(guild_id, channel_id) → POST /api/voice/connect → backend → Redis (backend:command) → discord-gateway → gateway connects → Redis (discord:voice:started) → WS event → komponen update status ``` ## Phase Breakdown ### Phase 1: Foundation (project scaffold + types + build) - [ ] Cargo workspace with `shared-types` and `frontend` crates - [ ] All Rust types (port 1:1 from TypeScript types + `@bete/shared`) - [ ] `Trunk.toml` + `index.html` + `main.rs` with `mount_to_body` - [ ] `rust-toolchain.toml` (nightly) - [ ] `.env` for API/WS URLs - [ ] Build verification: `trunk serve` produces a blank WASM page ### Phase 2: Skeleton (shell, auth, WS, API, UI primitives) - [ ] CSS Design System (`app.css`) — custom properties, component classes, keyframes, dark theme - [ ] `` component with providers - [ ] AuthOverlay + login flow - [ ] WebSocket singleton (connect, reconnect with exponential backoff, per-event channels) - [ ] API client (fetch wrapper with auth header, typed endpoint functions) - [ ] UI primitives: Button (7 variants), Badge (9), Card, Input, Select, Tabs, ScrollArea, Toast, Skeleton, StatusBadge, EmptyState, Modal - [ ] Layout shell: DashboardLayout, Sidebar, Header, TabStrip, MobileTabBar ### Phase 3: Messages Feature - [ ] MessagesPanel with AI status filter tabs - [ ] MessageFeed (infinite scroll via IntersectionObserver) - [ ] MessageCard with user grouping - [ ] MessageRow (content, edited/deleted indicators, Discord emoji, stickers, attachments, AI analysis box, severity badge, reanalyze) - [ ] ImageGrid (masonry grid from attachments/embeds/stickers) - [ ] ModerationAlertListener → Toast dispatch - [ ] Search (via `/api/analysis/search`) - [ ] Reanalyze (single + batch) ### Phase 4: Live Feature - [ ] VoiceConnectionCard (guild/channel selectors, join/disconnect/listen/transmit) - [ ] ActiveSpeakers (user list with speaking indicators) - [ ] AudioVisualizer (Canvas 2D, 32 bars, gradient, ResizeObserver) - [ ] MicLevelMeter (horizontal bar, 0-100%, green→red scale) - [ ] NowPlaying (current media + queue) - [ ] MusicSubPanel (URL input, volume slider, queue/skip/stop) - [ ] ScreenSubPanel (URL input, start/skip/stop) - [ ] RecordingsSubPanel (list + pagination + delete + status badges) - [ ] WaveformPlayer (Canvas 64 bars, AudioContext, play/pause/seek) - [ ] PCM audio playback (Web Audio API via web-sys) - [ ] Mic transmit (AudioContext → WebSocket binary frames) ### Phase 5: Dashboard Feature - [ ] StatsOverview (8 stat cards, top channels, moderation queue) - [ ] UserSummaryList (search + pagination + grid) - [ ] UserProfileDetail (stats + recent messages) - [ ] ChannelSummaryList (search + pagination + grid) - [ ] ChannelProfileDetail (stats + culture + recent messages) ### Phase 6: Polish - [ ] ParticleBackground (3 CSS glow orbs) - [ ] MascotChatbot (floating chat panel, API integration) - [ ] MascotImage (clickable mascot) - [ ] Edge cases: WS reconnect, stale data handling, concurrent requests - [ ] Mobile responsive (< md: sidebar → bottom nav, cards → single column) - [ ] Loading states + error boundaries + empty states for all panels - [ ] Theme toggle (dark/light via CSS custom properties) - [ ] Error animation states (Framer Motion fade/slide ported to CSS keyframes) ## API Client All existing endpoints mapped to typed Rust functions: ```rust // pattern pub async fn get_messages(guild_id: &str, params: &MessageParams) -> Result, ApiError>; // All endpoints (20+): // auth, messages, review, reanalyze, guilds, voice, media, recordings, // dashboard (stats/users/channels), ui-state, chat, search ``` Full list in `services/frontend/src/shared/api/client.ts` — 1:1 port. ## CSS Design System ### Source: `services/frontend/src/styles.css` (676 lines) **Port strategy:** 1. CSS custom properties (`:root` / `[data-theme="dark"]`) — port verbatim 2. Component classes (`.im-btn`, `.im-card`, etc.) — rename to `.btn`, `.card` etc., port styling 3. Keyframes — rename from `.im-*` prefix, port verbatim 4. Layout utility classes (`.flex`, `.grid`, `.gap-*`) — keep as utility classes or inline 5. Remove all Tailwind-specific directives **Component classes to define:** - `.btn` / `.btn-primary` / `.btn-secondary` / `.btn-ghost` / `.btn-destructive` / `.btn-outline` / `.btn-link` + `.btn-sm` / `.btn-lg` / `.btn-icon` - `.card` / `.card-elevated` / `.card-bordered` + `.card-header` / `.card-title` / `.card-content` / `.card-footer` - `.badge` / `.badge-primary` / `.badge-success` / `.badge-warning` / `.badge-destructive` / `.badge-outline` - `.input` / `.input-soft` + `.input-error` - `.tabs` / `.tab-list` / `.tab-trigger` / `.tab-content` - `.skeleton` / `.skeleton-circular` / `.skeleton-rectangular` - `.toast` / `.toast-success` / `.toast-error` / `.toast-warning` ## WebSocket Protocol (unchanged from current) - **Binary**: PCM audio `[4-byte userIdHash UInt32LE][Int16 PCM samples @ 24kHz mono]` - **JSON**: Typed envelope `{ type, data }` — 20+ event types (see `events.md`) Reconnection: exponential backoff with full jitter (1s base, 30s max, 20 attempts). ## Out of Scope (Phase 1) - React Native / mobile apps - PWA / service worker - E2E testing - Performance benchmarking - Bundle size optimization - Server-side rendering (Leptos SSR mode) - CI/CD integration These can be added after the CSR WASM version is stable. ## Dependencies ### Rust crates | Crate | Version | Purpose | |-------|---------|---------| | leptos | 0.7 (csr) | Reactive UI framework | | leptos-use | latest | `use_local_storage`, `use_interval`, `use_event_listener` etc. | | lucide-leptos | latest | SVG icons | | wasm-bindgen | 0.2 | JS/WASM bindings | | web-sys | 0.3 | Browser API bindings | | js-sys | 0.3 | JS type bindings | | gloo-net | 0.6 | HTTP fetch | | serde | 1 + derive | JSON serialization | | serde-wasm-bindgen | 0.6 | Serde ↔ JS interop | | wasm-logger | 0.2 | Logging to console | | console_error_panic_hook | 0.1 | Debug panic traces | ### web-sys features required WebSocket, CanvasRenderingContext2d, AudioContext, Window, Document, Element, HtmlElement, KeyboardEvent, Storage, IntersectionObserver, ResizeObserver, Url, Headers, Request, RequestInit, Response, HtmlInputElement, HtmlAudioElement, HtmlCanvasElement, MediaDevices, MediaStream, AudioBuffer, AudioBufferSourceNode ## Migration Notes - The React app (`services/frontend/`) stays intact until Leptos version is complete - No shared code between React and Leptos versions — complete rewrite - Build output: `trunk build` → `dist/` folder, deployable as static files alongside or replacing the current Vite build - Environment variables: port from `.env` to Rust compile-time config or runtime JS interop