From 3c2c1c3b15ca16b5892941681f388043cec28838 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Sat, 15 Aug 2026 19:23:17 +0700 Subject: [PATCH] Add Puppeteer scripts for navigation testing and debugging - 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. --- .../2026-08-13_gmw-frontend-greenfield.md | 418 ++++++++++++++++++ services/frontend/nav-debug.cjs | 48 ++ services/frontend/nav-test.cjs | 27 ++ services/frontend/nav-test2.cjs | 18 + services/frontend/nav-test4019.cjs | 26 ++ services/frontend/package.json | 1 + services/frontend/pnpm-lock.yaml | 200 ++++++++- .../src/components/ambient/ambient-canvas.tsx | 2 +- .../src/components/shell/nav-rail.tsx | 87 ++-- 9 files changed, 789 insertions(+), 38 deletions(-) create mode 100644 .hermes/plans/2026-08-13_gmw-frontend-greenfield.md create mode 100644 services/frontend/nav-debug.cjs create mode 100644 services/frontend/nav-test.cjs create mode 100644 services/frontend/nav-test2.cjs create mode 100644 services/frontend/nav-test4019.cjs diff --git a/.hermes/plans/2026-08-13_gmw-frontend-greenfield.md b/.hermes/plans/2026-08-13_gmw-frontend-greenfield.md new file mode 100644 index 0000000..67d30f9 --- /dev/null +++ b/.hermes/plans/2026-08-13_gmw-frontend-greenfield.md @@ -0,0 +1,418 @@ +# 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. Reuse `src/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 via `next/font/google` with CSS variables (keep current `--font-inter`/`--font-jetbrains-mono` names + add `--font-display`). + +### Layout & signature +- **No `card` with border.** Use tonal `--surface` blocks 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 `--signal` dot + 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 `RouteTransition` in `(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 `staggerChildren` variants; afterwards, quiet springs for hover/tap (`scale: 1.03` on interactive blocks, `whileTap` on buttons). +- **Layout animation:** `layout` prop on list items (messages rows, recording rows, queue) so add/remove/filter reflows smoothly; `layoutId` for shared-element transitions (ticker → detail modal on dashboard). +- **Live pulse:** `motion` drives the severity ticks / speaker rings with springs, not CSS `transition` alone. +- **`useReducedMotion()`** (from `motion/react`) gates ALL heavy motion; CSS `@media (prefers-reduced-motion: reduce)` additionally kills `scan`/`spin-disc` keyframes. Accessibility floor, non-negotiable. +- **No scroll-jacking, no marquee loops, no per-element confetti.** One moment per page. (`frontend-design` skill: "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: () => })`. Three ships in its own chunk, loaded only on those two routes. +- **WebGL guard:** if `!window.WebGLRenderingContext` or 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 on `document.hidden`; `dispose()` geometries/materials on unmount; particle count capped by `navigator.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_data` binary). **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` + `shadcn` deps. +- `recharts` (replace with custom SVG chart helpers in `src/components/charts/`). +- `src/app/globals.css` → rewrite (no `@import "shadcn/tailwind.css"`, no `--color-primary` teal, no `.glass*`, no `.text-gradient`/`.gradient-border` teal/purple, no bluish body texture). +- `src/components/layout/app-sidebar.tsx` (shadcn Sidebar) → replace with custom `Spine` nav. +- All `page.tsx`+`view.tsx` pairs → merge into single `page.tsx` per route. + +## 3. What to BUILD (new) +- New `globals.css` (tokens above + utilities + keyframes `scan`, `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.tsx` files (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 + client; 3D SignalField hero) + │ ├─ messages/ page.tsx REWRITE (server seeds + ; spine + severity ticks) + │ ├─ voice/ page.tsx REWRITE (server seeds + ; 3D OrbField hero) + │ ├─ media/ page.tsx REWRITE (server seeds + ; turntable disc) + │ ├─ recordings/ page.tsx REWRITE (server seeds + ; waveform rows) + │ ├─ moderation/ page.tsx REWRITE (server seeds + ; event-flow) + │ └─ analysis/ page.tsx REWRITE (client ; 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) +```tsx +// 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 ; +} + +// 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`. +- `three` only imported inside `src/components/three/*`; pages import those via `next/dynamic({ ssr: false })`. +- `motion` imported from `motion/react` only. +- 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`: remove `dependencies["@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`: confirm `onlyBuiltDependencies` keeps needed native builds, no broken shadcn postinstall. + +### PHASE 1 — Design tokens (globals.css) +- **T1.1** Rewrite `@theme { }` head: light `:root` palette 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-display` token; keep `--font-sans`/`--font-mono`. +- **T1.4** Add `.dark { }` override block with §0 dark values (warm charcoal). +- **T1.5** Replace `@layer base body` bg: warm dot-grid `radial-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 `::selection` signal-tinted. +- **T1.7** Delete `.glass`, `.glass-elevated`, `.glass-intense`, `.dark .glass-intense` utilities. +- **T1.8** Delete `.text-gradient` and `.gradient-border`. +- **T1.9** Add `.surface` utility (bg var(--surface), radius var(--r), padding). +- **T1.10** Add `.scan-tick` (1px animated pulse line, keyframe `scan`). +- **T1.11** Add `.ticker`, `.pill`, `.mono` utilities. +- **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.tsx` add `Bricolage_Grotesque` (`variable: "--font-display"`, subsets `["latin"]`, `display: "swap"`). +- **T2.2** Apply `inter.variable`, `jetbrainsMono.variable`, `bricolage.variable` to ``. +- **T2.3** Remove `import { Toaster } from "@/components/ui/sonner"`. +- **T2.4** Comment out `` 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`, variants `primary`/`ghost`/`danger`, `cn()` merge, `cursor-pointer`, `focus-visible:ring-2 ring-signal`, `whileTap` scale 0.97 gated by `useReducedMotion()`. +- **T3.2** `primitives/Input.tsx`: native ``, `bg-surface`, `rounded-[var(--r-control)]`, `mono` prop. +- **T3.3** `primitives/Select.tsx`: native `