- Created nav-debug.cjs to log anchor tags and simulate clicks on the Voice navigation link, capturing click events and page navigation. - Added nav-test.cjs to test the Voice link click and log the URL at various intervals, capturing any page errors. - Introduced nav-test2.cjs to check the presence of specific elements on the /voice/ page and log any console errors. - Implemented nav-test4019.cjs to monitor network requests and responses related to the Voice navigation, verifying button presence and click functionality.
35 KiB
GMW Frontend — Greenfield Rebuild (Visual-System Overhaul + Custom UI + Motion/3D)
For Hermes: Execute with
subagent-driven-development(one fresh subagent per task, two-stage review). Each task is 1–3 min, atomic, independently verifiable, committed after each. Reusesrc/lib/api/*,src/lib/ws/*,src/lib/types/*, hooks verbatim. Never invent endpoints.
Goal: Rebuild the GMW Discord-automod dashboard frontend from scratch — drop shadcn/ui + glass/teal/purple aesthetic entirely, replace with a custom, distinctive design system where every page has its own visual metaphor (no uniform bordered-card grid), and push the presentation layer with Framer Motion (motion) choreography + signature Three.js scenes. Re-integrate to the EXISTING backend API + WebSocket contract (do NOT touch the backend).
Architecture: Next.js 16 App Router (SSR) + React 19 + TS strict + Tailwind v4. Keep the plumbing (data contract), rebuild the skin + primitives + motion. Each route = one self-contained page.tsx (server fetch + client view in the same file via a "use client" sibling export). Custom SVG charts (no recharts). Custom micro-primitives (no shadcn/base-ui). New token system in globals.css. Motion: motion/react app-wide for page transitions, spring micro-interactions, layout animation. 3D: raw three (no react-three-fiber — leaner) in exactly TWO signature scenes, lazy-loaded client-only with graceful fallback. Deployed unchanged via existing flake + gmw-proxy nginx (:4009 → Next :4017).
Tech Stack: next@16, react@19, tailwindcss@4 (@import "tailwindcss"), next/font/google (Bricolage Grotesque + Inter + JetBrains Mono), swr (data revalidation), lucide-react (icons only), motion (Framer Motion successor — motion/react), three + @types/three (signature scenes only), clsx + tailwind-merge. Removed: @shadcn/react, @base-ui/react, recharts, shadcn CLI, cmdk, sonner, react-day-picker, embla-carousel-react, react-resizable-panels, input-otp, all 50 components/ui/*.
0. Design System (the creative core — read before coding)
Persona: Controlled UX Designer + a tactical "ops console" voice. Material honesty: hierarchy via scale/weight/tonal blocks, NOT borders/shadows. Per frontend-design skill principle — spend the boldness in ONE signature place per page, keep the rest disciplined. Motion is choreography, not confetti: one orchestrated moment per page, everything else quiet.
Palette (warm, signal-driven — NO teal/cyan/purple/blue gradients)
Light mode (:root):
--canvas: oklch(0.96 0.012 80) /* warm off-white, not cream */
--surface: oklch(0.92 0.014 80) /* tonal block, replaces bordered card */
--surface-2: oklch(0.88 0.016 80)
--ink: oklch(0.22 0.02 70) /* primary text */
--ink-soft: oklch(0.46 0.02 70) /* secondary text */
--hairline: oklch(0.22 0.02 70 / 0.10) /* structural rules ONLY, sparse */
--signal: oklch(0.78 0.17 125) /* lime — OK / live / primary accent */
--signal-ink: oklch(0.20 0.03 70) /* text ON signal */
--amber: oklch(0.80 0.15 70) /* WARN */
--vermilion: oklch(0.62 0.21 25) /* FLAGGED / destructive */
--ring: var(--signal)
Dark mode (.dark, default theme per next-themes):
--canvas: oklch(0.13 0.015 70) /* warm charcoal, not blue-black */
--surface: oklch(0.18 0.02 70)
--surface-2: oklch(0.23 0.022 70)
--ink: oklch(0.93 0.01 75)
--ink-soft: oklch(0.62 0.02 75)
--hairline: oklch(1 0 0 / 0.09)
--signal: oklch(0.88 0.18 125)
--signal-ink: oklch(0.18 0.03 70)
--amber: oklch(0.85 0.15 70)
--vermilion: oklch(0.68 0.21 25)
Three semantic signals reused everywhere: lime = OK/live, amber = warn, vermilion = flag/danger. This kills the purple-accent + teal-primary monotony.
Typography (3 roles, deliberate pairing — not "Inter everywhere")
- Display:
Bricolage Grotesque(700–800) — characterful grotesque for headers/big numbers. - Body/UI:
Inter(400–600). - Data/label:
JetBrains Mono(500/700) — all stats, timestamps, channel IDs, metrics. Load all three vianext/font/googlewith CSS variables (keep current--font-inter/--font-jetbrains-mononames + add--font-display).
Layout & signature
- No
cardwith border. Use tonal--surfaceblocks with generous radius (--r: 14px) and internal padding; separate blocks with whitespace + sparse hairlines only where structurally meaningful. - Signature element = "scan-tick": a 1px animated pulse line (CSS keyframe
scan) that marks every live/section header — NOT a card outline. Global canvas carries a faint warm dot-grid texture (low opacity) instead of the current bluish dotted radial-gradient. - Nav = left "spine": vertical rail of icon nodes joined by a hairline; active node gets a
--signaldot + label reveal. Collapses to a bottom tab-bar < 768px (CSS only, no JS sidebar primitive). - Header = "status bar": connection state (WS dot), guild selector, live clock — mono font, reads like an instrument readout.
0.1 Motion & 3D Layer (the "lebih kreatif" addition)
Motion rules (from motion/react)
- Page transitions: one shared
RouteTransitionin(dashboard)/layout.tsx—AnimatePresence mode="popLayout"+motion.div key={pathname}(fade + 8px rise + slight blur-out, ~220ms,easeOut). Consistent everywhere, zero per-page boilerplate. - Enter choreography (per page, ONE signature moment): staggered rise-in for the hero/ticker group using
staggerChildrenvariants; afterwards, quiet springs for hover/tap (scale: 1.03on interactive blocks,whileTapon buttons). - Layout animation:
layoutprop on list items (messages rows, recording rows, queue) so add/remove/filter reflows smoothly;layoutIdfor shared-element transitions (ticker → detail modal on dashboard). - Live pulse:
motiondrives the severity ticks / speaker rings with springs, not CSStransitionalone. useReducedMotion()(frommotion/react) gates ALL heavy motion; CSS@media (prefers-reduced-motion: reduce)additionally killsscan/spin-disckeyframes. Accessibility floor, non-negotiable.- No scroll-jacking, no marquee loops, no per-element confetti. One moment per page. (
frontend-designskill: "Satu momen orkestrasi biasanya lebih mengena daripada efek tersebar.")
3D rules (raw three, no R3F — lean bundle)
- Exactly two scenes, chosen because they carry real data meaning: Dashboard hero (
SignalField— a particle field whose pulse density reflects live activity) and Voice page (OrbField— speakers as glowing orbs whose height/ring radius reacts to who is speaking). Everything else stays 2D/motion. - Lazy + client-only:
next/dynamic(() => import("./SignalField"), { ssr: false, loading: () => <StaticFallback/> }). Three ships in its own chunk, loaded only on those two routes. - WebGL guard: if
!window.WebGLRenderingContextor context creation fails → render the static SVG/CSS fallback (a stylized 2D version of the same visual). Never blank. - Perf guardrails:
dpr: [1, 1.75],powerPreference: "high-performance",antialias: true; RAF loop paused ondocument.hidden;dispose()geometries/materials on unmount; particle count capped bynavigator.hardwareConcurrency+ viewport (Math.min(900, w*h/2000)). - Style: warm palette ONLY — signal-lime particles, amber/vermilion for flag/warn states; fog + soft additive blending for glow (no harsh white lights, no metallic PBR).
- Interactivity: subtle pointer parallax (camera lerp toward cursor) + gentle idle rotation. No drag/drop, no raycasting menus.
Per-page metaphor (kills monotony — each page feels different)
| Route | Metaphor | Signature visual | Motion / 3D |
|---|---|---|---|
/dashboard |
Live ops overview | 3D signal particle field hero + asymmetric ticker blocks + radial moderation gauge | 3D SignalField (reacts to activity), staggered ticker rise-in, gauge draws on mount |
/messages |
Transcript | Left channel timeline spine; right = flowing message entries with left severity tick (no bordered cards); search = command palette | layout on message rows, spring severity ticks, palette types in |
/voice |
Stage | 3D orb field of speakers + equalizer rings; activity = horizontal session ribbon | 3D OrbField (speaking → orb rises + ring pulses), session ribbon draws sequentially |
/media |
Turntable | Rotating disc now-playing; queue = borderless list | CSS 3D disc spin (spring on play/pause), queue layout reflow, progress bar springs |
/recordings |
Tape library | Rows with waveform thumbnail (custom SVG from duration) | Waveform bars spring on hover; new upload animates in (AnimatePresence) |
/moderation |
Security log | Vertical event flow with status nodes (dot + line), not a table of cards | Nodes pulse on live action; timeline draws in sequence |
/analysis |
Query console | Terminal-style search panel | Typing cursor + results stagger |
1. What to KEEP (reuse verbatim — correct + integrates to backend)
src/lib/api/server.ts— 11 server fetchers (getDashboardStats,getActivity,getMediaStatus,getConfig,getModerationStats/Actions,getGuilds,getVoiceStatus,getRecordings,getMessages). No change.src/lib/api/client.ts—apiRequest+ApiError. No change.src/lib/ws/*—connection.ts,context.tsx,types.ts(17 typed events:message_created/updated/deleted/analyzed,voice_*,media_state,voice_pcm_databinary). No change.src/lib/types/*— all interfaces. No change.src/lib/format.ts,src/lib/utils.ts(cn). No change.src/lib/navigation.ts—navItems. Keep but extend icons/labels if needed.- Hooks:
src/hooks/*(use-dashboard, use-media, use-messages, use-voice, use-moderation, use-recordings, use-guilds, use-config, use-chatbot-user, use-action, use-mobile),src/lib/hooks/use-mounted.ts. Reuse (verify no bad imports into deleted barrels). - Feature logic kept but reskinned:
src/components/media/music-player.tsx,src/components/voice/*,src/components/chatbot/*,src/components/messages/*,src/components/recordings/*,src/components/moderation/moderation-section.tsx,src/components/analysis/search-panel.tsx,src/components/dashboard/*(charts → rewritten as custom SVG).
2. What to DELETE
src/components/ui/*(all 50 shadcn primitives).components.json,@shadcn/react+@base-ui/react+shadcndeps.recharts(replace with custom SVG chart helpers insrc/components/charts/).src/app/globals.css→ rewrite (no@import "shadcn/tailwind.css", no--color-primaryteal, no.glass*, no.text-gradient/.gradient-borderteal/purple, no bluish body texture).src/components/layout/app-sidebar.tsx(shadcn Sidebar) → replace with customSpinenav.- All
page.tsx+view.tsxpairs → merge into singlepage.tsxper route.
3. What to BUILD (new)
- New
globals.css(tokens above + utilities + keyframesscan,eq,fade-up,spin-disc). src/components/primitives/— minimal custom:Button,Input,Select,Dialog,Tooltip,Badge,Progress,Avatar,Skeleton,Toast,Sheet.src/components/motion/—RouteTransition.tsx,Stagger.tsx,variants.ts.src/components/three/—SignalField.tsx,OrbField.tsx,WebGLGuard.tsx,StaticFallback.tsx,useThreeScene.ts.src/components/charts/—Sparkline,AreaActivity,RadialGauge,SessionRibbon,Waveform.src/components/layout/—Spine.tsx,StatusBar.tsx,ThemeToggle.tsx(reskin).- 7 merged
page.tsxfiles (one per route) implementing the metaphors above. src/app/layout.tsx— root (fonts + ThemeProvider + Toaster),src/app/(dashboard)/layout.tsx— providers + Spine + StatusBar + RouteTransition + MiniPlayer + Chatbot,src/app/page.tsx→ redirect/dashboard.
4. Target File & Folder Structure (authoritative)
Everything below services/frontend/src/ is the new tree. REWRITE replaces existing; NEW creates; DELETE removes. The app/ route tree collapses page.tsx+view.tsx into single page.tsx files containing BOTH server fetch (default export) and client view ("use client" named export in same file).
services/frontend/
├─ package.json REWRITE (drop shadcn/base-ui/recharts; add motion, three, @types/three)
├─ pnpm-workspace.yaml REWRITE (onlyBuiltDependencies: keep build list minimal)
├─ components.json DELETE (shadcn registry config — no longer used)
├─ next.config.ts KEEP (output: standalone, trailingSlash, images.unoptimized)
├─ tsconfig.json KEEP (paths "@/*" → src/*, strict)
├─ postcss.config.mjs KEEP (@tailwindcss/postcss)
├─ biome.json KEEP
└─ src/
├─ app/
│ ├─ layout.tsx REWRITE (3 fonts + ThemeProvider + custom Toaster; rm sonner)
│ ├─ globals.css REWRITE (new token system §0; rm glass/teal/purple)
│ ├─ page.tsx KEEP (redirect → /dashboard/)
│ └─ (dashboard)/
│ ├─ layout.tsx REWRITE (providers + Spine + StatusBar + RouteTransition + MiniPlayer + Chatbot; rm shadcn Sidebar)
│ ├─ dashboard/ page.tsx REWRITE (server fetch + <DashboardView/> client; 3D SignalField hero)
│ ├─ messages/ page.tsx REWRITE (server seeds + <MessagesView/>; spine + severity ticks)
│ ├─ voice/ page.tsx REWRITE (server seeds + <VoiceView/>; 3D OrbField hero)
│ ├─ media/ page.tsx REWRITE (server seeds + <MediaView/>; turntable disc)
│ ├─ recordings/ page.tsx REWRITE (server seeds + <RecordingsView/>; waveform rows)
│ ├─ moderation/ page.tsx REWRITE (server seeds + <ModerationView/>; event-flow)
│ └─ analysis/ page.tsx REWRITE (client <AnalysisView/>; query console)
├─ components/
│ ├─ ui/ DELETE (all 50 shadcn primitives)
│ ├─ primitives/ NEW (Button, Input, Select, Dialog, Tooltip, Badge, Progress, Avatar, Skeleton, Toast, Sheet, index.ts)
│ ├─ motion/ NEW (variants.ts, Stagger.tsx, RouteTransition.tsx)
│ ├─ three/ NEW (WebGLGuard, SignalField, OrbField, StaticFallback, useThreeScene)
│ ├─ charts/ NEW (Sparkline, AreaActivity, RadialGauge, SessionRibbon, Waveform)
│ ├─ layout/ REWRITE (Spine NEW, StatusBar NEW, ThemeToggle REWRITE, app-sidebar DELETE)
│ ├─ dashboard/ REWRITE (stat-card DELETE; activity-chart/hourly/top-channels/moderation-donut/users/channels/reactions REWRITE)
│ ├─ messages/ REWRITE (message-card DELETE; message-list/detail/detail-view/ai-status-badge/ai-analysis-panel/attachments-grid/lightbox/search-overlay REWRITE)
│ ├─ voice/ REWRITE (voice-connection-card/connection-card/microphone-card DELETE; speaker-waveform/active-speakers-panel/activity-timeline/mic-control/listen-control REWRITE)
│ ├─ media/ REWRITE (music-player, mini-player REWRITE)
│ ├─ recordings/ REWRITE (recording-card DELETE; recording-player REWRITE)
│ ├─ moderation/ REWRITE (moderation-section REWRITE)
│ ├─ analysis/ REWRITE (search-panel REWRITE)
│ ├─ chatbot/ REWRITE (chatbot-container, chat-panel REWRITE; chatbot-context, index KEEP)
│ └─ shared/ REWRITE (empty-state, error-state, loading-skeleton, error-boundary, guild-selector REWRITE; index KEEP)
├─ hooks/ KEEP (verify no bad imports)
├─ lib/
│ ├─ api/ KEEP (server.ts, client.ts, index.ts)
│ ├─ ws/ KEEP (connection.ts, context.tsx, types.ts, ws-hook.ts)
│ ├─ types/ KEEP (all interfaces)
│ ├─ hooks/ KEEP (use-media-player.tsx, use-mounted.ts)
│ ├─ audio/ KEEP (voice PCM decode helpers if present)
│ ├─ format.ts KEEP
│ ├─ utils.ts KEEP (cn)
│ └─ navigation.ts KEEP
└─ (public assets) KEEP
4.1 Single-file page pattern (mandatory)
// server component (default export) — runs on the server, fetches initial data
import { getX, getY } from "@/lib/api/server";
import { XView } from "./page"; // self-import of the named client export
export default async function Page() {
const [a, b] = await Promise.allSettled([getX(), getY()]);
return <XView initialA={a.status === "fulfilled" ? a.value : undefined}
initialB={b.status === "fulfilled" ? b.value : undefined} />;
}
// client component (named export) — hydrated, takes initialData as SWR fallback
"use client";
export function XView({ initialA, initialB }: Props) {
const { data } = useX(initialA); // SWR fallbackData = initialA
}
Self-referencing the named export keeps the file single-artifact while satisfying Next's RSC boundary (default = server, named = client). Tabs live inside XView.
4.2 Import rules (lint gate)
- No
@/components/ui/*(deleted) — all UI via@/components/primitives. threeonly imported insidesrc/components/three/*; pages import those vianext/dynamic({ ssr: false }).motionimported frommotion/reactonly.- All data:
@/lib/api/server(server) /@/lib/api/client(client) — never invented endpoints.
TASKS (granular — every file is its own task)
PHASE 0 — Dependency surgery
- T0.1 Edit
package.json: removedependencies["@base-ui/react"]. - T0.2 Remove
dependencies["@shadcn/react"]. - T0.3 Remove
dependencies["shadcn"]. - T0.4 Remove
dependencies["recharts"]. - T0.5 Remove
dependencies["cmdk"]. - T0.6 Remove
dependencies["sonner"]. - T0.7 Remove
dependencies["react-day-picker"]. - T0.8 Remove
dependencies["embla-carousel-react"]. - T0.9 Remove
dependencies["react-resizable-panels"]. - T0.10 Remove
dependencies["input-otp"]. - T0.11 Add
dependencies["motion"]: "^12.0.0",dependencies["three"]: "^0.180.0",devDependencies["@types/three"]: "^0.180.0". - T0.12
rm -f pnpm-lock.yaml && pnpm install(regenerate lockfile). - T0.13 Verify
pnpm ls recharts @shadcn/react @base-ui/react→ empty;pnpm ls motion three @types/three→ present. - T0.14
cat pnpm-workspace.yaml: confirmonlyBuiltDependencieskeeps needed native builds, no broken shadcn postinstall.
PHASE 1 — Design tokens (globals.css)
- T1.1 Rewrite
@theme { }head: light:rootpalette from §0 (canvas/surface/ink/ink-soft/hairline/signal/signal-ink/amber/vermilion/ring). - T1.2 Add radius tokens
--r: 14px,--r-panel: 12px,--r-control: 8px,--r-pill: 9999px. - T1.3 Add
--font-displaytoken; keep--font-sans/--font-mono. - T1.4 Add
.dark { }override block with §0 dark values (warm charcoal). - T1.5 Replace
@layer base bodybg: warm dot-gridradial-gradient(oklch(0.45 0.03 70 / 0.05) 1px, transparent 1px)+ faint warm glow; remove old bluish radial layers. - T1.6 Retint scrollbar thumb to warm
oklch(0.4 0.02 70 / 0.2); keep::selectionsignal-tinted. - T1.7 Delete
.glass,.glass-elevated,.glass-intense,.dark .glass-intenseutilities. - T1.8 Delete
.text-gradientand.gradient-border. - T1.9 Add
.surfaceutility (bg var(--surface), radius var(--r), padding). - T1.10 Add
.scan-tick(1px animated pulse line, keyframescan). - T1.11 Add
.ticker,.pill,.monoutilities. - T1.12 Add keyframes
scan,eq,fade-up,spin-disc(keep used existing ones if still referenced). - T1.13 Add
@media (prefers-reduced-motion: reduce)kill switch for scan/eq/spin-disc/pulse-ring/shimmer. - T1.14 Remove
@import "shadcn/tailwind.css";(line 3); verify nothing else depends on shadcn CSS vars. - T1.15 Verify
grep -c "0.52 0.17 215\|0.55 0.2 280" src/app/globals.css→0. - T1.16
pnpm biome check src/app/globals.css→ no errors.
PHASE 2 — Root layout + fonts
- T2.1 In
layout.tsxaddBricolage_Grotesque(variable: "--font-display", subsets["latin"],display: "swap"). - T2.2 Apply
inter.variable,jetbrainsMono.variable,bricolage.variableto<html>. - T2.3 Remove
import { Toaster } from "@/components/ui/sonner". - T2.4 Comment out
<Toaster />temporarily (re-enabled after T3.10). - T2.5 Keep
suppressHydrationWarning,ThemeProvider(defaultTheme dark, enableSystem false). - T2.6
npx tsc --noEmit(fonts only; rest may still error until primitives exist).
PHASE 3 — Custom primitives (replace 50 shadcn ui)
- T3.1
primitives/Button.tsx:motion.button, variantsprimary/ghost/danger,cn()merge,cursor-pointer,focus-visible:ring-2 ring-signal,whileTapscale 0.97 gated byuseReducedMotion(). - T3.2
primitives/Input.tsx: native<input>,bg-surface,rounded-[var(--r-control)],monoprop. - T3.3
primitives/Select.tsx: native<select>styled,bg-surface. - T3.4
primitives/Dialog.tsx: native<dialog>+showModal(), warm::backdrop,AnimatePresence,onClose. - T3.5
primitives/Tooltip.tsx: CSS group-hover popover. - T3.6
primitives/Badge.tsx: tonal pill,variant→bg-{tone}/15 text-{tone}(signal/amber/vermilion/neutral). - T3.7
primitives/Progress.tsx: SVG track +motionfill,value/max, signal color. - T3.8
primitives/Avatar.tsx:<img>+ initials fallback, signal bg, size prop. - T3.9
primitives/Skeleton.tsx: shimmer block (signal-tinted),aria-hidden. - T3.10
primitives/Toast.tsx:ToastProvidercontext + portal,useToast(), motion slide-in, auto-dismiss. - T3.11
primitives/Sheet.tsx: mobile drawer (translate-xspring), overlay,open/onClose. - T3.12
primitives/index.tsre-export all 11. - T3.13 Re-enable
<Toaster />inlayout.tsx(T2.4). - T3.14 Verify
npx tsc --noEmiton primitives;grep -rl "@/components/ui/" src/components/primitives→ empty.
PHASE 4 — Motion foundation
- T4.1
motion/variants.ts: exportspring,ease,fadeUp,stagger(per §0.1). - T4.2
motion/Stagger.tsx:StaggerGroup+StaggerItem("use client"). - T4.3
motion/RouteTransition.tsx:"use client",usePathname,AnimatePresence mode="popLayout", reduced-motion fallback to plain<div>. - T4.4 Verify
npx tsc --noEmiton motion;motion/reactimport resolves.
PHASE 5 — Custom SVG charts (replace recharts)
- T5.1
charts/Sparkline.tsx:<svg>polyline frompoints:number[], signal stroke, no axes. - T5.2
charts/AreaActivity.tsx: filled<path>area, low-opacity signal gradient,pathLengthdraw gated by reduced-motion. - T5.3
charts/RadialGauge.tsx:<circle>arcstroke-dasharray, center mono label. - T5.4
charts/SessionRibbon.tsx: horizontal segments per speaker duration. - T5.5
charts/Waveform.tsx: bars from deterministic seed, spring scaleY on hover. - T5.6 Verify
npx tsc --noEmiton charts; norechartsimport.
PHASE 6 — Three.js foundation (lazy, guarded)
- T6.1
three/WebGLGuard.tsx:"use client", detect webgl2/webgl, renderchildrenorfallback. - T6.2
three/useThreeScene.ts: shared hook — renderer init (dpr:[1,1.75],powerPreference), RAF withdocument.hiddenpause,dispose()on unmount, resize observer. - T6.3
three/SignalField.tsx:PointsBufferGeometry (~min(900, w*h/2000)), additive blend, signal-lime, fog, idle rotation + sine drift,activityprop, pointer parallax. UsesuseThreeScene. - T6.4
three/OrbField.tsx: per-speakerSphere, y-scale + ring lerp to speaking, tones signal/idle/vermilion. - T6.5
three/StaticFallback.tsx: 2D SVG/CSS silhouette for both scenes. - T6.6 Verify
grep -rl "from \"three\"" src | grep -v "components/three"→ empty;npx tsc --noEmiton three.
PHASE 7 — Layout shell
- T7.1
layout/Spine.tsx:"use client", vertical rail fromnavItems, icon node + hairline, active signal dot + label reveal (motion spring),max-md:bottom tab-bar. - T7.2
layout/StatusBar.tsx:"use client", page title + WS status dot (motion pulse) +GuildSelector+ live clock (mono) +ThemeToggle. - T7.3
layout/ThemeToggle.tsx: restyle, keepnext-themeslogic, motion icon swap. - T7.4 Rewrite
(dashboard)/layout.tsx: keep SWRConfig/WsProvider/MediaPlayerProvider/ChatbotProvider + sync functions verbatim; swapAppSidebar→Spine, header→StatusBar, wrap children inRouteTransition; removeSidebarInset/SidebarTrigger/Separator; keep MiniPlayer+ChatbotContainer. - T7.5 Delete
layout/app-sidebar.tsx. - T7.6 Verify
grep -rl "components/ui/sidebar\|app-sidebar" src→ empty;npx tsc --noEmit.
PHASE 8 — Dashboard page + components
- T8.1 Rewrite
dashboard/page.tsx: default asyncgetDashboardStats+getActivity→<DashboardView>; named"use client"view with useStats/useActivity, 3D hero + tickers + tabs. - T8.2 Add
WebGLGuard+SignalFieldhero withactivityratio; overlay headline (Bricolage) +<RadialGauge>. - T8.3 Build asymmetric ticker row with
StaggerGroup+ 4.surfaceblocks (mono number + label +<Sparkline>); inline (replaces stat-card). - T8.4 Delete
dashboard/stat-card.tsx. - T8.5 Reskin
dashboard/activity-chart.tsx→charts/AreaActivity(daily). - T8.6 Reskin
dashboard/hourly-activity-chart.tsx→charts/AreaActivity(hourly). - T8.7 Reskin
dashboard/top-channels-chart.tsx→charts/+.surface. - T8.8 Reskin
dashboard/moderation-donut.tsx→charts/RadialGauge. - T8.9 Reskin
dashboard/users-section.tsx→.surface. - T8.10 Reskin
dashboard/channels-section.tsx→.surface. - T8.11 Reskin
dashboard/reactions-section.tsx→.surface. - T8.12 Verify
grep -rl "components/ui/card" src/app/\(dashboard\)/dashboard src/components/dashboard→ empty;npx tsc --noEmit.
PHASE 9 — Messages page + components
- T9.1 Rewrite
messages/page.tsx: defaultgetMessages(guildId)(+channels) →<MessagesView>; client spine + entries. - T9.2 Delete
messages/message-card.tsx. - T9.3 Rewrite
messages/message-list.tsx: left timeline spine + right severity-tick entries (surface+border-l-2lime/amber/vermilion, motion spring tick),layoutreflow. - T9.4 Rewrite
messages/message-detail.tsx→.surface. - T9.5 Rewrite
messages/message-detail-view.tsx→.surfacepane. - T9.6 Rewrite
messages/ai-status-badge.tsx→primitives/Badge. - T9.7 Rewrite
messages/ai-analysis-panel.tsx→.surface. - T9.8 Rewrite
messages/attachments-grid.tsx→.surfacegrid. - T9.9 Rewrite
messages/lightbox.tsx→primitives/Dialog. - T9.10 Rewrite
messages/search-overlay.tsx→ console palette, type-in animation,primitives/Dialog. - T9.11 Verify no
components/ui/cardin messages tree;npx tsc --noEmit.
PHASE 10 — Voice page + components
- T10.1 Rewrite
voice/page.tsx: defaultgetVoiceStatus()→<VoiceView>; clientWebGLGuard+OrbFieldhero + ribbon + tabs. - T10.2 Delete
voice/voice-connection-card.tsx,connection-card.tsx,microphone-card.tsx. - T10.3 Rewrite
voice/speaker-waveform.tsx→ SVG ring / eq bars. - T10.4 Rewrite
voice/active-speakers-panel.tsx→.surface. - T10.5 Rewrite
voice/activity-timeline.tsx→charts/SessionRibbon. - T10.6 Rewrite
voice/mic-control.tsx→primitives/Button. - T10.7 Rewrite
voice/listen-control.tsx→primitives/Button. - T10.8 Verify
npx tsc --noEmit; nocomponents/ui/cardin voice tree.
PHASE 11 — Media page + components
- T11.1 Rewrite
media/page.tsx: defaultgetMediaStatus()→<MediaView>; client turntable disc + transport + queue. - T11.2 Rewrite
media/music-player.tsx: CSS-3D disc (spin-disc, pause when not playing, spring on play/pause), mono meta,primitives/Buttontransport,.surfacequeue rows withlayout. - T11.3 Rewrite
media/mini-player.tsx→ compact.surface. - T11.4 Verify
npx tsc --noEmit; nocomponents/ui/cardin media tree.
PHASE 12 — Recordings page + components
- T12.1 Rewrite
recordings/page.tsx: defaultgetRecordings(50)→<RecordingsView>; client rowsAnimatePresence+layout, livevoice_recording_uploadedprepend. - T12.2 Delete
recordings/recording-card.tsx. - T12.3 Rewrite
recordings/recording-player.tsx→.surfacerow +charts/Waveform+primitives/Button/Dialogplay/delete. - T12.4 Verify
npx tsc --noEmit.
PHASE 13 — Moderation + Analysis pages
- T13.1 Rewrite
moderation/page.tsx: defaultgetModerationStats/Actions→<ModerationView>; client vertical event-flow, live actions pulse-in. - T13.2 Rewrite
moderation/moderation-section.tsx→ event-flow, no Card/table. - T13.3 Rewrite
analysis/page.tsx: client<AnalysisView/>terminal console (primitives/Inputmono + blinking caret,.surfaceresults staggered). - T13.4 Rewrite
analysis/search-panel.tsx→ terminal style. - T13.5 Verify
npx tsc --noEmit.
PHASE 14 — Chatbot + shared + final cleanup
- T14.1 Rewrite
chatbot/chatbot-container.tsx→.surface, keep drag/minimize. - T14.2 Rewrite
chatbot/chat-panel.tsx→ bubbles viaAnimatePresence,primitives/*. - T14.3 Keep
chatbot/chatbot-context.tsx+index.ts. - T14.4 Rewrite
shared/empty-state.tsx→ tonal. - T14.5 Rewrite
shared/error-state.tsx. - T14.6 Rewrite
shared/loading-skeleton.tsx→primitives/Skeleton. - T14.7 Rewrite
shared/error-boundary.tsx. - T14.8 Rewrite
shared/guild-selector.tsx→primitives/Select. - T14.9
rm -rf src/components/ui && rm -f components.json. - T14.10
grep -rn "components/ui/\|@shadcn\|@base-ui\|recharts" src→ MUST be empty. - T14.11
grep -rl "from \"recharts\"\|@base-ui\|@shadcn" src→ empty (double-check). - T14.12 Verify
npx tsc --noEmitacross wholesrc.
PHASE 15 — Build + lint gate
- T15.1
cd services/frontend && npx tsc --noEmit→ 0 errors. - T15.2
pnpm biome check→ fix all issues (noanyin new files). - T15.3
pnpm build(standalone) → success, emits.next/standalone/server.js. - T15.4 Inspect
.next/static/chunks/for three-heavy chunk loaded only on dashboard/voice; confirm NOT in/dashboard/initial SSR HTML. - T15.5 Confirm
pnpm-lock.yamlpresent (reproducible flake install). - T15.6
grep -c "0.52 0.17 215\|0.55 0.2 280" .next/static/css/*.css→ 0.
PHASE 16 — Local runtime smoke test (no prod)
- T16.1 Start local standalone:
GMW_BACKEND_URL=http://127.0.0.1:4001 PORT=4017 node .next/standalone/server.js &. - T16.2
curl -s -o /dev/null -w "%{http_code}"for all 7 routes → 200. - T16.3
curl /dashboard/ | grep -o "Bricolage\|signal\|surface"→ present. - T16.4
curl /_next/static/css/*.css | grep "0.52 0.17 215\|0.55 0.2 280"→ empty. - T16.5 Headless browser
/dashboard/+/voice/WebGL on: no console errors,<canvas>present,<StaticFallback/>NOT rendered. - T16.6 Same pages WebGL off:
<StaticFallback/>renders, no crash. - T16.7 Kill local server. Do NOT touch prod unit.
- T16.8 Confirm 7 routes 200 + no console errors in T16.5/16.6.
PHASE 17 — Flake + staging deploy
- T17.1 Inspect
flake.nixfrontend drv:filterSourceignoresout/.next/node_modules;pnpm-lock.yamlincluded. - T17.2
nix build .#gmw-frontend --impure --sandbox-off→ succeeds. - T17.3
nix copyfrontend drv to VPS into staging profile (test port e.g. 4217). - T17.4 Create/adjust staging systemd unit with
PORT=4217exported BEFOREnode server.js(LIDM PORT bug). - T17.5
sudo systemctl restart gmw-frontend-staging;curlstaging → 200. - T17.6 Browser-check staging
/dashboard/+/voice/(3D visible, fallback test). - T17.7 Verify staging CSS has no old teal/purple; new design renders.
PHASE 18 — Production swap (CONFIRM WITH USER FIRST)
- T18.1 STOP — send staging screenshots/URL; await explicit approval before touching prod.
- T18.2 On approval:
nix-env --profile /nix/var/nix/profiles/gmw-frontend --set <new-drv>. - T18.3 Confirm
gmw-frontend.serviceexportsPORT=4017before exec. - T18.4
sudo systemctl restart gmw-frontend. - T18.5
curlall 7 routes onhttps://imphnen.asepharyana.my.id→ 200. - T18.6 Browser verify prod: new design, old teal gone, 3D scenes render.
- T18.7
journalctl -u gmw-frontend -f5 min; confirm WS reconnect + live features. - T18.8 Notify user with before/after notes; keep rollback plan (
nix-env --set <previous>; systemctl restart).
Risks / Trade-offs
- Scope: 7 pages + charts + primitives + motion + 2 three scenes + layout. Big but mechanical; each task is isolated (~90 atomic tasks).
- Bundle weight:
threeadds ~150KB gz but ONLY on dashboard/voice routes (lazy chunk,ssr:false).motion~35KB gz app-wide — acceptable. - WebGL compatibility: covered by
WebGLGuard+ static fallback. Old devices / strict privacy browsers never blank. - Motion excess: risk of "AI-generated" scattered animation. Guard: one signature moment per page, shared variants, reduced-motion gates.
- Feature regressions: Voice PCM playback, media transport, chatbot drag — logic preserved, only skin changes. Smoke test (T16) catches SSR breaks; live WS/3D needs real backend (staging T17).
- Removed deps: dropping
recharts/sonner/cmdkmeans rewriting charts + toasts + search palette — accounted for in Phases 3/5/9. - Next standalone PORT bug:
server.jsmay not readPORT— ensure unit exportsPORT=4017before exec (T17.4/T18.3). - three + React 19: raw three avoids R3F compat surface; lifecycle (dispose + RAF) handled in T6.2.
- next-themes: keep (light/dark toggle); default dark.
Open questions (answer before T18)
- Q1: Deploy to prod now or staging-only first? (Recommend staging + screenshot review.)
- Q2: Keep
react-day-picker/emblaif any page still needs them? (Plan assumes no — verify in T14.10 grep.) - Q3: Any brand name/wordmark change from "Discord Automod"? (Keep "Bete" identity unless told.)
- Q4: 3D depth — full 3D scenes on dashboard+voice as specced, or also a 3D accent on media (disc)? (Default: dashboard+voice only; media disc stays CSS 3D.)