feat(frontend): rebuild as SSR with server-authoritative shared state
Rombak total alur data frontend: dari static-export CSR (tiap browser fetch sendiri + akumulasi state voice per-tab) jadi server-side rendering. Frontend (Next.js): - next.config: output export -> standalone; halaman jadi server components - server data layer baru src/lib/api/server.ts (GMW_BACKEND_URL, no window) - dashboard/media/messages/moderation/recordings/voice page -> RSC yang fetch backend di render-time, seed ke client view (SWR fallbackData) - hook-hook utama terima initialData -> first paint data server, revalidate SWR setelahnya, tanpa spinner-blank-load - messages: guild/channel/tab/selected dibaca dari URL di server, page awal di-fetch server-side Shared realtime state (voice) server-authoritative: - backend src/modules/voice/live-speaker.ts: agregat voice_active_user dari gateway jadi snapshot authoritatif (single source of truth semua browser) - GET /api/voice/status kini include activeSpeakers - WS initial states kirim voice_state snapshot saat connect (late join langsung dapat state yang sama, bukan daftar kosong) - useSpeakers seed dari server snapshot + voice_state full-replace + voice_active_user delta upsert Deploy: - flake.nix: frontend package build SSR standalone (server.js wrapper, GMW_FRONTEND_PORT=4017); proxy nginx template proxy / -> Next server, /api + /ws tetap ke backend :4001
This commit is contained in:
+30
-13
@@ -3,26 +3,41 @@
|
||||
Next.js 16 (App Router), React 19, TypeScript strict, Tailwind v4, shadcn/ui, base-ui.
|
||||
|
||||
Key points:
|
||||
- **All pages** are `"use client"` — the dashboard is fully client-rendered
|
||||
- **API client** at `src/lib/api/` — fetch-based, covers all 30+ backend endpoints
|
||||
- **WebSocket** at `src/lib/ws/` — auto-reconnecting client with typed event subscriptions
|
||||
- **Static export**: `output: "export"` in next.config.ts, served via nginx
|
||||
- **Server-side rendered (SSR)** — `output: "standalone"` in next.config.ts; pages
|
||||
are React Server Components that fetch initial data from the backend at
|
||||
render-time, then hydrate interactive client components (no blank-spinner-first-load).
|
||||
- **Server data layer** at `src/lib/api/server.ts` — server-only fetchers that
|
||||
call the backend directly via `GMW_BACKEND_URL` (default `http://127.0.0.1:4001`).
|
||||
Never import from a client component.
|
||||
- **API client** at `src/lib/api/client.ts` — browser-side fetch for live ops,
|
||||
same-origin through the reverse proxy.
|
||||
- **WebSocket** at `src/lib/ws/` — auto-reconnecting client with typed event
|
||||
subscriptions. Realtime state (voice, media, messages) stays client-side.
|
||||
- **Shared realtime state is server-authoritative**: the backend aggregates the
|
||||
gateway's `voice_active_user` deltas into a live speaker snapshot
|
||||
(`GET /api/voice/status` → `activeSpeakers`, plus WS `voice_state` sent on
|
||||
connect). Every browser converges on the same voice state; `useSpeakers`
|
||||
seeds from the server snapshot instead of accumulating per-tab.
|
||||
- **No authentication**: all endpoints are public
|
||||
|
||||
## Data flow (match these — do not invent endpoints)
|
||||
|
||||
```
|
||||
Discord → discord-gateway → Redis pub/sub → backend (Express :4001) ←→ frontend
|
||||
↑ REST /api/* (same-origin)
|
||||
└ WS /ws (events + PCM binary)
|
||||
Discord → discord-gateway → Redis pub/sub → backend (Express :4001) ←→ Next.js SSR
|
||||
↑ REST /api/* (server: GMW_BACKEND_URL
|
||||
└ WS /ws (events + PCM binary) 127.0.0.1:4001)
|
||||
↑ browser WS (same-origin /ws)
|
||||
```
|
||||
|
||||
- **Base URL**: API + WS default to same-origin. `gmw-proxy` nginx (:4009)
|
||||
proxies `/api` and `/ws` to the backend on :4001. Public host:
|
||||
`imphnen.asepharyana.my.id` (Caddy reverse proxy → :4009).
|
||||
- Local dev overrides: `NEXT_PUBLIC_API_URL` and `NEXT_PUBLIC_WS_URL`
|
||||
(e.g. https://imphnen.asepharyana.my.id).
|
||||
- **Never hardcode a host** in api/ws clients — same-origin or env override only.
|
||||
- **Rendering**: `gmw-proxy` nginx (:4009) proxies `/` → Next standalone server
|
||||
(:4017, `node .next/standalone/server.js`), and `/api` + `/ws` → backend :4001.
|
||||
Public host: `imphnen.asepharyana.my.id` (Caddy reverse proxy → :4009).
|
||||
- **SSR seed pattern**: each `page.tsx` is a server component that fetches via
|
||||
`src/lib/api/server.ts` and passes typed data to a `view.tsx` client
|
||||
component; the hooks take `initialData` as SWR `fallbackData`.
|
||||
- Local dev overrides: `NEXT_PUBLIC_API_URL` and `NEXT_PUBLIC_WS_URL` for the
|
||||
browser; `GMW_BACKEND_URL` for the server.
|
||||
- **Never hardcode a host** in api/ws clients — same-origin/env or GMW_BACKEND_URL only.
|
||||
|
||||
## Backend response shapes that bite
|
||||
|
||||
@@ -34,3 +49,5 @@ Discord → discord-gateway → Redis pub/sub → backend (Express :4001) ←→
|
||||
- Dashboard endpoints: `/api/dashboard/stats|users|channels` (+ `/:id` details).
|
||||
- Channel/guild names live inside `message.metadata` JSON (`channel.channelName`),
|
||||
not top-level.
|
||||
- `GET /api/voice/status` now includes `activeSpeakers` (authoritative shared
|
||||
snapshot from `src/modules/voice/live-speaker.ts` on the backend).
|
||||
|
||||
Reference in New Issue
Block a user