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
3.1 KiB
3.1 KiB
Bete Frontend — Project Overview
Next.js 16 (App Router), React 19, TypeScript strict, Tailwind v4, shadcn/ui, base-ui.
Key points:
- 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 viaGMW_BACKEND_URL(defaulthttp://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_userdeltas into a live speaker snapshot (GET /api/voice/status→activeSpeakers, plus WSvoice_statesent on connect). Every browser converges on the same voice state;useSpeakersseeds 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) ←→ Next.js SSR
↑ REST /api/* (server: GMW_BACKEND_URL
└ WS /ws (events + PCM binary) 127.0.0.1:4001)
↑ browser WS (same-origin /ws)
- Rendering:
gmw-proxynginx (: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.tsxis a server component that fetches viasrc/lib/api/server.tsand passes typed data to aview.tsxclient component; the hooks takeinitialDataas SWRfallbackData. - Local dev overrides:
NEXT_PUBLIC_API_URLandNEXT_PUBLIC_WS_URLfor the browser;GMW_BACKEND_URLfor the server. - Never hardcode a host in api/ws clients — same-origin/env or GMW_BACKEND_URL only.
Backend response shapes that bite
GET /api/chat/history→{ history: ChatbotHistoryRow[], total }where each row is{ id, user_id, user_message, bot_response, context, created_at }. Map rows to display messages inchatbot-context.tsx.message_deletedWS payload →{ id, deleted_at }(an object, not a string).voice_recording_uploadedWS payload has noduration_bytes(REST rows do).- Dashboard endpoints:
/api/dashboard/stats|users|channels(+/:iddetails). - Channel/guild names live inside
message.metadataJSON (channel.channelName), not top-level. GET /api/voice/statusnow includesactiveSpeakers(authoritative shared snapshot fromsrc/modules/voice/live-speaker.tson the backend).