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:
asepharyana committed 2026-08-07 10:44:03 +07:00
1 parent aa440eda69
commit f20889868d
32 files changed
+1556 -956

No files matched your search

+22 -7
View File
@@ -1,20 +1,35 @@
import useSWR from "swr";
import { moderationApi } from "@/lib/api";
import type { ModerationStats } from "@/lib/types";
import type { ModerationAction, ModerationStats } from "@/lib/types";
export function useModerationStats() {
return useSWR<ModerationStats>(["moderation-stats"], () =>
moderationApi.getStats(),
export function useModerationStats(initialData?: ModerationStats) {
return useSWR<ModerationStats>(
["moderation-stats"],
() => moderationApi.getStats(),
{ fallbackData: initialData },
);
}
export function useModerationActions(status?: string, actionType?: string) {
export function useModerationActions(
status?: string,
actionType?: string,
initialData?: ModerationAction[],
) {
const key = [
"moderation-actions",
status ?? "__all__",
actionType ?? "__all__",
];
return useSWR(
["moderation-actions", status ?? "__all__", actionType ?? "__all__"],
key,
async () => {
const res = await moderationApi.listActions(100, status, actionType);
return res.data;
},
{ keepPreviousData: true },
{
keepPreviousData: true,
fallbackData:
!status && !actionType && initialData ? initialData : undefined,
},
);
}