docs: leptos frontend rewrite design spec

This commit is contained in:
asepharyana
2026-07-03 06:07:12 +07:00
parent 0926f235cb
commit 4fb59cda95
@@ -0,0 +1,311 @@
# 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-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
- [ ] `<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:
```rust
// 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:**
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