Files
GMW/services/frontend/src/app/(dashboard)/messages/page.tsx
T
asepharyana f20889868d 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
2026-08-07 10:44:03 +07:00

42 lines
1.2 KiB
TypeScript

/**
* Messages page — Server Component.
*
* Reads the URL (guild/channel/tab/selected) on the server and, when a guild
* is already selected, fetches the first message page server-side so the
* initial list is server-rendered, not a client round-trip.
*/
import { getMessages, type MessagePageResult } from "@/lib/api/server";
import MessagesView from "./view";
export default async function MessagesPage({
searchParams,
}: {
searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
const sp = await searchParams;
const guild = typeof sp.guild === "string" ? sp.guild : "";
const channel = typeof sp.channel === "string" ? sp.channel : "";
const selected = typeof sp.selected === "string" ? sp.selected : null;
const tab =
typeof sp.tab === "string" && ["all", "images", "review"].includes(sp.tab)
? (sp.tab as "all" | "images" | "review")
: "all";
let initialPage: MessagePageResult | undefined;
if (guild) {
initialPage = await getMessages(guild, channel || undefined).catch(
() => undefined,
);
}
return (
<MessagesView
initialGuild={guild}
initialChannel={channel}
initialDetailId={selected}
initialTab={tab}
initialMessagePage={initialPage}
/>
);
}