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:
@@ -17,7 +17,11 @@ import { EmptyState, LoadingSkeleton } from "@/components/shared";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useModerationActions, useModerationStats } from "@/hooks";
|
||||
import { renderMessageContent } from "@/lib/format";
|
||||
import type { ModerationAction, ModerationActionType } from "@/lib/types";
|
||||
import type {
|
||||
ModerationAction,
|
||||
ModerationActionType,
|
||||
ModerationStats,
|
||||
} from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ACTION_META: Record<
|
||||
@@ -82,13 +86,20 @@ const EMPTY_ACTION_RATE = {
|
||||
failed_rate: 0,
|
||||
};
|
||||
|
||||
export function ModerationSection() {
|
||||
export function ModerationSection({
|
||||
initialStats,
|
||||
initialActions,
|
||||
}: {
|
||||
initialStats?: ModerationStats;
|
||||
initialActions?: ModerationAction[];
|
||||
} = {}) {
|
||||
const [status, setStatus] = useState<string>("");
|
||||
const [actionType, setActionType] = useState<string>("");
|
||||
const { data: stats } = useModerationStats();
|
||||
const { data: stats } = useModerationStats(initialStats);
|
||||
const { data: actions, isLoading: actionsLoading } = useModerationActions(
|
||||
status,
|
||||
actionType,
|
||||
initialActions,
|
||||
);
|
||||
|
||||
const s = stats ?? EMPTY_ACTION_RATE;
|
||||
@@ -159,7 +170,7 @@ export function ModerationSection() {
|
||||
</div>
|
||||
|
||||
{/* Timeline */}
|
||||
{actionsLoading ? (
|
||||
{actionsLoading && !actions ? (
|
||||
<LoadingSkeleton count={6} height="h-16" />
|
||||
) : !actions || actions.length === 0 ? (
|
||||
<GlassCard className="p-6">
|
||||
|
||||
Reference in New Issue
Block a user