13 KiB
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
<App>
<ToastProvider>
{!authenticated && !is_public → <AuthOverlay />}
<DashboardLayout>
<ParticleBackground />
<Sidebar>
<NavLinks> Messages | Live | Dashboard </NavLinks>
<MascotImage on:click → toggle MascotChatbot />
</Sidebar>
<Header>
<Logo />
<ConnectionIndicator />
<ThemeToggle />
</Header>
<TabStrip />
{active_tab == "messages" → <MessagesPanel />}
{active_tab == "live" → <LivePanel />}
{active_tab == "dashboard" → <DashboardPanel />}
</DashboardLayout>
<MascotChatbot />
</ToastProvider>
</App>
State Architecture
Contexts (provided at <App> level via provide_context):
| Context | Key Signals | Persisted |
|---|---|---|
| AuthContext | authenticated: ReadSignal<bool> |
sessionStorage |
| WsContext | status: ReadSignal<WsStatus>, per-event channels |
— |
| UiContext | active_tab: RwSignal<Tab>, selected_guild: RwSignal<Option<String>>, 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-typesandfrontendcrates - All Rust types (port 1:1 from TypeScript types +
@bete/shared) Trunk.toml+index.html+main.rswithmount_to_bodyrust-toolchain.toml(nightly).envfor API/WS URLs- Build verification:
trunk serveproduces 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 <App>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:
// pattern
pub async fn get_messages(guild_id: &str, params: &MessageParams) -> Result<PageResult<MessageRecord>, 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:
- CSS custom properties (
:root/[data-theme="dark"]) — port verbatim - Component classes (
.im-btn,.im-card, etc.) — rename to.btn,.cardetc., port styling - Keyframes — rename from
.im-*prefix, port verbatim - Layout utility classes (
.flex,.grid,.gap-*) — keep as utility classes or inline - 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 (seeevents.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
.envto Rust compile-time config or runtime JS interop