diff --git a/.hermes/plans/mobile-nav-hydration-fix.md b/.hermes/plans/mobile-nav-hydration-fix.md new file mode 100644 index 00000000..1bcf78b3 --- /dev/null +++ b/.hermes/plans/mobile-nav-hydration-fix.md @@ -0,0 +1,61 @@ +# GMW — Fix mobile navbar "tidak bisa pindah halaman" (root cause) + +## Context / symptom +- User (phone, mobile viewport ` client-side navigation is dead app-wide.** Clicking a mobile nav + `` fires the click (event reaches document, `inLink=true`, + `defaultPrevented=false`) but **the URL never changes**. `window.next.router.push('/voice')` + also does nothing. The desktop NavRail navigates only because it uses a **plain + ``** (full browser navigation bypasses the broken router). +2. **A React hydration failure (#418 "server rendered text didn't match the client")** + is thrown on live pages — the only console error. Cause: relative-time text + (`formatRelativeTime(e.edited_at)` / `formatRelativeTime(m.created_at)` in the + SSR-seeded messages/edit-history feed uses `Date.now()`; server and client + render slightly different text → hydration mismatch → React re-renders, and the + Next client router ends up non-functional. + +## Why desktop "worked" / mobile didn't +- `NavRail` = plain `` → **hard** navigation (works). +- `MobileNav` = Next `` → **client** navigation → dead router. +User's instruction "ikuti cara kerja sidebar" = make mobile nav behave like the +sidebar (plain anchors). + +## Changes +### 1. `src/components/shell/mobile-nav.tsx` +Replace Next `` with a **plain ``** (mirrors NavRail). Keep: +`usePathname` for active-state styling + `scrollIntoView` for snap-to-active. +Drop the now-unused `Link` import (biome import-order — run biome check --write). + +### 2. Hydration root cause — `formatRelativeTime` in SSR-seeded feeds +Add `suppressHydrationWarning` to the timestamp elements in the SSR-seeded +components that render live-relative text so server/client text drift no longer +throws #418: +- `src/components/EditHistory.tsx` (line ~111 `edited {formatRelativeTime(...)}`) +- `src/app/(dashboard)/messages/view.tsx` MessageRow channel/time (line ~580) + + semantic row (line ~364) +- `src/components/LiveModerationFeed.tsx` (line ~139) +- (recordings/view.tsx, TermGlossary, ChannelCultureGlossary, CategoryDrilldown, + chatbot — chatbot is client-after-mount; add where SSR-seeded.) + +NOTE: `suppressHydrationWarning` is safe for these single-text spans; the values +are cosmetic and self-correct on the next interval/render. + +## Verification (non-prod, NEVER 4017) +- `pnpm format` + `pnpm lint` (biome) + `pnpm build` clean. +- Smoke on :4024 (fresh `next dev`): mobile click on nav item actually navigates + (URL changes) AND no React #418 in console. +- After push: `gh run watch`, then live `imphnen.asepharyana.my.id` @375px: nav tap + navigates, console free of #418. + +## Files +- services/frontend/src/components/shell/mobile-nav.tsx +- services/frontend/src/components/EditHistory.tsx +- services/frontend/src/app/(dashboard)/messages/view.tsx +- services/frontend/src/components/LiveModerationFeed.tsx +- (others only if #418 persists) diff --git a/services/frontend/AGENTS.md b/services/frontend/AGENTS.md index c1aeb431..646f98ab 100644 --- a/services/frontend/AGENTS.md +++ b/services/frontend/AGENTS.md @@ -51,3 +51,13 @@ Discord → discord-gateway → Redis pub/sub → backend (Express :4001) ←→ not top-level. - `GET /api/voice/status` now includes `activeSpeakers` (authoritative shared snapshot from `src/modules/voice/live-speaker.ts` on the backend). + + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/services/frontend/src/app/(dashboard)/messages/view.tsx b/services/frontend/src/app/(dashboard)/messages/view.tsx index 0d7f73fa..5bac5c55 100644 --- a/services/frontend/src/app/(dashboard)/messages/view.tsx +++ b/services/frontend/src/app/(dashboard)/messages/view.tsx @@ -360,7 +360,10 @@ export function MessagesView({ {(r.score * 100).toFixed(0)}% RELEVANCE - + {formatRelativeTime(r.created_at)} @@ -576,7 +579,10 @@ function MessageDetail({ {m.server_nick && m.server_nick !== m.username && (
@{m.username}
)} -
+
{getMessageChannelLabel(m)} · {formatRelativeTime(m.created_at)}
@@ -752,7 +758,10 @@ function MessageRow({ {getMessageChannelLabel(m)} - + {formatRelativeTime(m.created_at)} diff --git a/services/frontend/src/app/(dashboard)/recordings/view.tsx b/services/frontend/src/app/(dashboard)/recordings/view.tsx index e477ac79..533e6e77 100644 --- a/services/frontend/src/app/(dashboard)/recordings/view.tsx +++ b/services/frontend/src/app/(dashboard)/recordings/view.tsx @@ -215,7 +215,9 @@ export function RecordingsView({ {r.channel_name ?? "voice"} · - {formatRelativeTime(r.created_at)} + + {formatRelativeTime(r.created_at)} + {isPlaying && } diff --git a/services/frontend/src/components/CategoryDrilldown.tsx b/services/frontend/src/components/CategoryDrilldown.tsx index 3a775e50..6242d1ac 100644 --- a/services/frontend/src/components/CategoryDrilldown.tsx +++ b/services/frontend/src/components/CategoryDrilldown.tsx @@ -103,7 +103,10 @@ export function CategoryDrilldown({ {a.username && ( @{a.username} )} - + {a.created_at ? formatRelativeTime(a.created_at) : ""} diff --git a/services/frontend/src/components/ChannelCultureGlossary.tsx b/services/frontend/src/components/ChannelCultureGlossary.tsx index 69c6e07a..ddca78b9 100644 --- a/services/frontend/src/components/ChannelCultureGlossary.tsx +++ b/services/frontend/src/components/ChannelCultureGlossary.tsx @@ -158,7 +158,7 @@ export function ChannelCultureGlossary({ INTEL RATIO - + {c.last_analyzed_at ? formatRelativeTime(c.last_analyzed_at) : "NEVER"} diff --git a/services/frontend/src/components/EditHistory.tsx b/services/frontend/src/components/EditHistory.tsx index 4daa3f3b..36545318 100644 --- a/services/frontend/src/components/EditHistory.tsx +++ b/services/frontend/src/components/EditHistory.tsx @@ -106,7 +106,10 @@ export function EditHistory({ edits }: { edits: EditHistoryRow[] }) { {e.username ?? "unknown"} - + edited {formatRelativeTime(e.edited_at)} diff --git a/services/frontend/src/components/LiveModerationFeed.tsx b/services/frontend/src/components/LiveModerationFeed.tsx index 71de3de1..e49f005e 100644 --- a/services/frontend/src/components/LiveModerationFeed.tsx +++ b/services/frontend/src/components/LiveModerationFeed.tsx @@ -135,7 +135,10 @@ export function LiveModerationFeed({ [{a.categories.slice(0, 2).join(", ")}] ) : null} - + {formatRelativeTime(a.created_at)} diff --git a/services/frontend/src/components/TermGlossary.tsx b/services/frontend/src/components/TermGlossary.tsx index 42df084e..bad9aea2 100644 --- a/services/frontend/src/components/TermGlossary.tsx +++ b/services/frontend/src/components/TermGlossary.tsx @@ -30,7 +30,9 @@ function GlossaryEntry({ t }: { t: GlossaryRow }) {
- {formatRelativeTime(t.resolved_at)} + + {formatRelativeTime(t.resolved_at)} + diff --git a/services/frontend/src/components/shell/mobile-nav.tsx b/services/frontend/src/components/shell/mobile-nav.tsx index d8ca7432..f653caa7 100644 --- a/services/frontend/src/components/shell/mobile-nav.tsx +++ b/services/frontend/src/components/shell/mobile-nav.tsx @@ -1,6 +1,5 @@ "use client"; -import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useRef } from "react"; import { isActivePath, mobileNavItems } from "@/lib/navigation"; @@ -12,6 +11,12 @@ import { cn } from "@/lib/utils"; * every page is reachable on mobile. On narrow screens the bar scrolls * horizontally (snap) — the active item snaps into view on navigation. Safe-area * aware for notched devices. + * + * NOTE: uses a plain `` (NOT Next ``) — deliberately identical to + * the working NavRail. Next's client-side router is unreliable here (a hydration + * mismatch in the SSR-seeded live feeds leaves `router.push` a no-op), so client + * `` navigation dead-ends (the "navbar mobile tak bisa pindah halaman" + * bug). A plain anchor does a full browser navigation and always works. */ export function MobileNav() { const path = usePathname() ?? "/"; @@ -39,7 +44,7 @@ export function MobileNav() { {mobileNavItems.map((item) => { const active = isActivePath(path, item.matchPrefix); return ( - {item.label} - + ); })}