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
shutdown() awaited httpServer.close(), which waits for ALL open
connections. A lingering WS/keep-alive socket left the process zombie
forever after an uncaughtException (e.g. pg 'Connection terminated
unexpectedly' to imrnes) — no exit, so systemd Restart=always could
never revive it; /api/guilds returned 502 until manual restart.
Add 10s force-exit timer in shutdown(); clear it on clean completion.
Gateway publishes {playing, activeMode, musicVolume, current, queue} but the
backend MediaState interface + normalizeMediaState dropped activeMode, so the
FE's 'Screen share active' / 'Music playing' badge never rendered. Carry it
through so the FE↔BE media contract stays in sync.
Audit voice (kirim/terima/music/screenshare) menemukan 3 masalah:
1. media:queue SILENT no-op — backend publish {source,mode} tapi gateway
handler baca payload.url → selalu 'received without a URL'. Backend
sekarang kirim {url,mode}, gateway terima url ATAU source (robust).
2. Voice connect gagal diam-diam saat user pilih channel tanpa permission
(joinable=false, contoh Music 32/64/128/256k). Backend+gateway sekarang
expose joinable; FE disable channel 'no akses' + empty state.
3. FE tidak kasih feedback saat connect gagal — tambah toast.error dengan
pesan dari backend.
Verified live: @discordjs/voice connect ke Lofi Radio joinable sukses
(VOICE READY, DAVE session OK) — pipeline voice sebenarnya sehat, masalah
utama UX. media:queue fix akan di-verify setelah deploy.
Backend:
- New chatbot.tools.ts: 4 tools (get_server_stats, get_top_channels,
get_recent_activity, get_top_flagged) with real DB executors
- chatbot.service: agentic loop — stream:true, parse SSE, execute
tool_calls, feed results back, up to 4 rounds
- controller: resolve userId from X-User-Id header (no-login device
uuid) with auth middleware precedence; history/clear scoped per user
Frontend:
- use-chatbot-user: mint UUID in localStorage, send as X-User-Id
- chatbotApi.send/getHistory/clearHistory accept userId header
- client.ts: apiRequest supports custom headers per call
- provider: history load + send + clear keyed to device user id
The voice_recordings table has no duration column, but the backend aliased
duration_bytes = size_bytes (file size in bytes) and RecordingCard divided it
by 60 as if it were seconds — a 3MB MP3 rendered as a nonsensical '55924:3'
fake timestamp. Drop the fabricated field and show real file size instead.
Also render edited_content fallback in the analysis search results for
consistency with message cards/detail.
Backend:
- GET /api/dashboard/activity?days=1..90 — daily buckets (messages,
flagged, active_users) + hourly distribution last 24h
- clamped days param, reuses existing indexes (idx_messages_created,
ai_status_created)
Frontend:
- ActivityChart: area chart messages+flagged per day, 7/14/30d range
- HourlyActivityChart: 24h bars with peak highlight
- ModerationDonut: clean/flagged/warned/error breakdown with live
summary line (server X% clean)
- Dashboard layout: activity 2/3 + donut 1/3, hourly + top channels
Verified: endpoint returns real data from prod DB (784/1897/8 msgs
per day), tsc clean backend+frontend.
packages/shared dihapus (5802d02), modul pindah ke src/shared/. Update
import placeholder.test.ts (gateway + backend) ke path lokal + aktifkan
tests/ di backend vitest config dengan alias @. Sebelumnya vitest run
gateway selalu gagal; sekarang 60+36 tests pass.
Build & Deploy / build-and-push (backend) (push) Failing after 35s
Build & Deploy / build-and-push (discord-gateway) (push) Failing after 25s
Build & Deploy / build-and-push (proxy) (push) Failing after 25s
- Remove pnpm workspace, moon repo, and all monorepo tooling
- Delete packages/shared/, embed shared code directly into each service
- Copy packages/shared/src/* -> services/backend/src/shared/ and services/discord-gateway/src/shared/
- Replace all @bete/shared imports with @/shared/ path alias
- Remove @bete/shared workspace dependency from both services
- Update root package.json scripts from --filter to --prefix
- Rewrite Dockerfiles to build each service standalone
- Clean up biome.json, .gitignore, remove root drizzle.config.ts
Build & Deploy / build-and-push (discord-gateway) (push) Failing after 2m22s
Build & Deploy / build-and-push (backend) (push) Failing after 3m22s
Build & Deploy / build-and-push (proxy) (push) Successful in 1m36s
Build & Deploy / deploy (push) Skipped
- Consolidate all DB schema definitions into packages/shared as single source of truth
- Migrate backend from raw SQL to Drizzle ORM across all modules
- Extract frontend inline UI into separate component files
- Refactor discord-gateway circuitBreaker into conversationState + moderationState
- Convert messageStore to Proxy singleton pattern
- Add validateBody/validateQuery middleware + Zod schemas for API endpoints
- Modernize Docker builds with multi-stage + pnpm deploy
- Migrate CI/CD from deployment to image-based pipeline
- Remove 60+ unused/dead files (~15K lines)
- Update color scheme from sky-blue to teal-cyan
- Move DB connection management to @bete/shared/database
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Added new dependencies for Next.js and lucide-react in pnpm-workspace.yaml.
- Refactored DashboardPage component to improve readability and error handling.
- Enhanced Header component to display error status with an alert icon.
- Updated MobileTabBar and Sidebar components to use a centralized tabs definition.
- Improved ChannelsView in dashboard-panel to handle channel fetching more cleanly.
- Fixed ActiveSpeaker type to use camelCase for userId.
- Updated MessagesPanel to handle guildId checks more gracefully.
- Adjusted API calls in dashboard and messages to align with backend expectations.
- Refined type definitions across various interfaces for consistency and clarity.
- Fix noImplicitAnyLet: add type to let match variable
- Fix noAssignInExpressions: use matchAll() + for-of instead of while
- Suppress useExhaustiveDependencies in mascot scroll effect
- Suppress useSemanticElements for message card click handler
- Remove auth module (auth.routes.ts, /api/auth/login)
- Remove adminAuth middleware from voice and media routes
- Remove adminAuth() function from shared middlewares
- Remove auth-related e2e test
- Clean up .env.example
Messages, analysis, mascot-chat, recordings, ui-state, and guilds
endpoints are now accessible without authentication.
Only voice (connect/disconnect/transmit) and media (queue/skip/stop/volume)
remain behind the admin password guard.
- SELECT transcription FROM voice_recordings fails because table has no such column
- Removed non-existent transcription from SELECT and interface
- Previous sql.raw() parameter binding also broken — fixed with proper drizzle sql``
- Hand-rolled , params with sql.raw() didn't actually bind values
- Drizzle's sql.raw() just inserts literal text — no parameter binding
- Replaced with proper sql`` tagged templates + sql.join() for conditions
- Each filter now correctly binds via drizzle's parameterized query
- Video frame extraction via ffmpeg (4 key frames per video → vision LLM)
- Video display in FE MessageCard with HTML5 <video> player
- Reply/forward/crosspost indicator in FE + pipeline in DG/BE
- Fix: missing sanitizeAiContent + escapeXml in media path (prompt injection)
- Optimize: text-only batch results saved to DB immediately, no longer wait for media analysis
- BE mapper/schema/repo: add reference fields (is_reply, is_forward, etc.)