Commit Graph
100 Commits
Author SHA1 Message Date
asepharyana 0a5254bf20 feat(ai-moderation): enhance context handling with structured XML blocks and user profiles 2026-08-10 16:46:55 +07:00
asepharyana 4a51f3055c ci(deploy): push builds to attic binary cache (attic.asepharyana.my.id) 2026-08-10 15:27:59 +07:00
asepharyana 185d81f0e0 feat(ai-moderation): reset offensive nickname instead of deleting message
When the ONLY violation is offensive_username (message content clean):
- Message is NOT deleted (nickname-only violation bypasses auto-delete)
- Member's server nickname is reset to default username via
  setNickname(null) (Discord shows the global username again)
- Action 'reset_nickname' logged to moderation_actions; cooldown
  10min per guild:user (LRU) so repeated messages by same member
  don't hammer the Discord PATCH
- Config: AUTO_NICKNAME_RESET_ENABLED / AUTO_NICKNAME_RESET_COOLDOWN_MS
2026-08-10 11:48:27 +07:00
asepharyana ecbb538c9f feat(ai-moderation): use per-server nickname (displayName) in analysis payload
- resolveDisplayName(): member.displayName from captured metadata,
  falls back to global username
- Applied to context lines, target message blocks, and media message
  blocks — LLM sees the name the channel actually sees (nickname can
  carry moderation signal itself)
2026-08-10 11:36:37 +07:00
asepharyana 4049ab4201 feat(ai-moderation): rich context + link media vision analysis
- Conversation context recency gates (GAP_MS/MAX_AGE_MS): drop stale
  messages before silence gaps; cold_start anchor + flow descriptor
  tells LLM whether conversation is ongoing or restarted
- [location] block: channel name, thread name, nsfw/age flags from
  captured metadata (thread names instead of bare IDs)
- Link media -> multimodal: text-batch URL fetches that resolve to
  images now run vision analysis (bounded 15s) and switch prompt to
  mixed mode; <web_content> gains og:title for page context
- pnpm-workspace.yaml: approve sharp build script (unblocks install)
2026-08-10 11:26:26 +07:00
asepharyana 5d094829c4 fix(ui): fit select popup to content and wrap long items
A single long guild name was clipped (whitespace-nowrap + narrow min-width),
so the dropdown rendered as a tiny 144px box with truncated text. Size the
popup to fit-content up to the available width and let option text wrap.
2026-08-07 19:29:36 +07:00
asepharyana abbd78f42b fix(ui): theme popover/input tokens and open select below trigger
Select dropdowns (voice tab, guild selector) rendered with a transparent
background because --color-popover/--color-input were undefined, and the
popup overlapped the trigger due to alignItemWithTrigger. Define the missing
theme tokens (popover, popover-foreground, input, secondary) and default the
select popup to open below the trigger.
2026-08-07 18:18:14 +07:00
asepharyana 4f9d4a5c7d refactor: remove unused UI components and replace GlassCard with Card in voice components
- Deleted Item, Kbd, Marker, Message, NativeSelect, Questionnaire, Spinner components.
- Replaced GlassCard with Card in VoiceActivityTimeline, VoiceConnectionCard, ListenControl, MicControl, and SpeakerWaveform components.
- Introduced AppSidebar and ThemeToggle components for improved navigation and theme management.
2026-08-07 17:33:12 +07:00
asepharyana 2c995b41d7 feat: add new UI components including RadioGroup, Resizable, Sidebar, Spinner, Table, Toast, and ToggleGroup
- Implemented RadioGroup and RadioGroupItem for radio button functionality.
- Created ResizablePanelGroup, ResizablePanel, and ResizableHandle for resizable panels.
- Developed Sidebar component with context for state management and various subcomponents (SidebarTrigger, SidebarMenu, etc.).
- Added Spinner component for loading indicators.
- Introduced Table component with TableHeader, TableBody, TableFooter, and related subcomponents for structured data display.
- Built Toast component for notifications with customizable actions and icons.
- Implemented ToggleGroup and ToggleGroupItem for toggle button functionality with context support.
2026-08-07 16:11:33 +07:00
asepharyana 18dd6a56ba feat(media): loop mode + high-quality OggOpus music playback
- Loop: toggle via POST /api/media/loop → COMMAND_MEDIA_LOOP; gateway
  replays finished music track on natural end (queue untouched); status
  payload exposes loop flag; FE tombol Loop di music-player + mini-player.
- Kualitas suara: music playback sekarang di-transcode sekali via ffmpeg ke
  OggOpus 48kHz stereo 192kbps dengan volume di-bake ke encode — menghindari
  double lossy encode (inlineVolume) yang bikin suara buram. Screen share
  tetap pakai jalur lama.
- Backend: MediaState.loop, setLoop service, route + schema validation.
2026-08-07 14:53:37 +07:00
asepharyana a690e5b63e refactor(media): remove volume control from FE & BE
Volume sudah di-set default 0.3 di gateway (suara kecil saat play), dan
user bisa naikin sendiri di Discord (command media:volume) — jadi kontrol
volume lewat dashboard tak perlu. Hapus:
- BE: POST /api/media/volume route, mediaVolumeSchema, setVolume service
- FE: useMediaVolume hook, mediaApi.volume, slider volume di music-player
  + mini-player, field volume/setVolume di MediaPlayerProvider
Pertahankan COMMAND_MEDIA_VOLUME di gateway (masih dipakai command DC)
dan mic volume (terpisah, tetap di voice page).
2026-08-07 14:27:33 +07:00
asepharyana 62ffb676f9 feat(media): default music volume 30% instead of 100%
Volume play music terlalu besar buat user — default sekarang 0.3 (30%)
di semua layer: player gateway (musicVolume=0.3), backend state/schema
(default 0.3), dan UI slider (fallback 0.3). User tetap bisa naikin
manual via slider volume di dashboard.
2026-08-07 13:41:12 +07:00
asepharyana 4797aca20f ci(deploy): poll service readiness instead of fixed 3s sleep
is-active after sleep 3 false-fails when the unit is still activating
(e.g. Next standalone boot >3s) — exit 3 flagged the deploy red even though
the service came up fine. Poll is-active up to 30s and only fail if it never
reaches 'active'.
2026-08-07 11:22:29 +07:00
asepharyana 42b8afd412 fix(flake): purge dangling pnpm symlinks in standalone tree
noBrokenSymlinks fails frontend build: .next/standalone/node_modules/.pnpm/
node_modules/semver -> missing target. The standalone server never resolves
pnpm's hoisted .pnpm dir (it bundles its own node_modules) — delete broken
symlinks before install so stdenv check passes.
2026-08-07 10:57:18 +07:00
asepharyana 7575a701bd style(backend): biome format — organize imports + spacing (live-speaker/redis-bridge/voice.service) 2026-08-07 10:49:16 +07:00
asepharyana 9f91155944 ci(deploy): build+deploy frontend package (SSR standalone server)
Sebelumnya frontend hanya static export yang di-serve nginx di dalam package
proxy. Sekarang frontend = runtime mandiri (Next.js standalone :4017) yang
nginx proxikan ('/' -> Next server, '/api' + '/ws' -> backend :4001). Tambah
'frontend' ke matrix deploy agar dideploy + memulai unit gmw-frontend.
2026-08-07 10:46:02 +07:00
asepharyana f20889868d 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
2026-08-07 10:44:03 +07:00
asepharyana aa440eda69 fix(gateway): music/screen playback heads — read yt-dlp headers from stderr, drop no-simulate
Music playback produced no audio: with `-o -` yt-dlp streams media on
stdout and emits its `--print` title/duration headers on stderr, but
resolveMediaUrl read them from stdout — stripping two binary 'lines' off
the WebM container and corrupting the stream (player 'playing' but silent).
Now headers are read from stderr and the stdout media stream is returned
untouched.

Screenshare was failing with EACCES: getDirectScreenInput used --no-simulate,
making yt-dlp write .f*.part files into the read-only Nix store CWD. Dropped
it — simulate mode still returns requested_formats[].url in the JSON.

Adds tests/mediaResolve.test.ts (stderr-header + untouched-stream regression).
2026-08-06 21:26:50 +07:00
asepharyana f251e69f51 fix(backend): force-exit failsafe so shutdown never hangs
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.
2026-08-05 23:58:30 +07:00
asepharyana 9a2fa999bf fix(voice): proper shadcn select dropdowns + top reactors leaderboard
- ui/select: trigger default w-full h-9 (was w-fit h-8 — selects rendered
  tiny/misaligned); callers keep size override via className
- VoiceConnectionCard: labeled full-width h-10 selects (Server/Guild +
  Voice Channel), guild icon + name in options, channel type icon +
  'no akses' tag, empty states, htmlFor/id a11y wiring
- GuildSelector sidebar + messages channel filter bumped to match
- Backend GET /api/dashboard/reactors: top users by net reactions given
  (adds-removes) + messages_reacted + emojis_used
- Reactions tab: second 'Top reaktor' leaderboard panel
2026-08-05 11:29:56 +07:00
asepharyana f999be4fa0 feat(dashboard): add moderation log page + message edit history
- Backend moderation module: GET /api/moderation/stats (per-status + failed rate)
  + GET /api/moderation/actions (filter by status/actionType, cursor paging),
  joins messages for target username + content
- Message GET /api/messages/detail/:id now returns edit_count + edit_history
  (old_content snapshots from message_edits, newest first)
- FE: new /moderation page — summary cards (total/executed/failed/pending +
  failed-rate), status+type filter chips, timeline rows with action icon,
  target user, reason, status badge, timestamps, error text
- FE: message detail shows 'Riwayat edit' panel with previous versions
2026-08-05 11:11:10 +07:00
asepharyana a309570d29 feat(dashboard): expose trust reputation + reactions leaderboard
- GET /api/dashboard/reactions: top reacted messages (net add-remove),
  joined with message content, channel name, top 3 emoji breakdown
- Users tab: colored trust tier badge (Trusted/Netral/At Risk/Kritis)
  in list rows + detail panel (was plain number badge)
- Dashboard: new Reactions sub-tab with leaderboard
  (rank, emoji cluster, message, author, channel, count)
2026-08-05 10:20:12 +07:00
asepharyana 2f51f94610 fix(moderation): remove manual reanalyze triggers — auto-recovery only
Manual per-message and batch reanalyze buttons/endpoints let anyone
re-queue arbitrary messages for LLM analysis, burning AI credits on
spam. Removed:
- FE: Reanalyze buttons in message list, search panel, and messages page
- FE: useReanalyze/useReanalyzeBatch hooks + messagesApi methods
- BE: POST /api/messages/:id/reanalyze and /reanalyze-batch endpoints
- BE: markForReanalysis/reanalyzeErrorBatch service+repository methods

Recovery of failed messages is fully automatic: the discord-gateway
startPendingAIAnalysisWorker retries 'pending' (batch path) and
'error/analysis_incomplete' (individual path) messages on
AI_ANALYSIS_RECOVERY_INTERVAL_MS.
2026-08-04 15:37:56 +07:00
asepharyana 88484f12a9 ci: add Nix GC cleanup job on VPS after deploy 2026-08-04 13:57:44 +07:00
asepharyana a2542493cd fix(voice): screen share now carries audio — merge DASH video+audio into single NUT input
getDirectVideoUrl used yt-dlp --get-url with bestvideo+bestaudio, which
prints the video-only and audio-only URLs on SEPARATE lines. Only the
first (video-only) line was used, so ffmpeg had no audio track and the
GoLive stream had no sound.

Replace with getDirectScreenInput which:
- uses --dump-single-json to fetch both fresh URLs in ONE yt-dlp run
  (signature URLs expire quickly)
- returns the merged progressive URL directly when one exists
- otherwise merges the video-only + audio-only DASH URLs locally via a
  child ffmpeg into a single NUT stream consumed as a Readable
- tracks the merge ffmpeg process in cleanup() so shutdown kills it too

Verified end-to-end with real YouTube URLs: yt-dlp pair → live ffmpeg
merge (NUT) → H264+opus transcode yields both streams. Added
tests/screenShareInput.test.ts covering URL / DASH-pair / error paths.
2026-08-04 13:28:55 +07:00
aseph f84bf723c5 ci: use free GHA Nix cache (disable FlakeHub cache, not subscribed) 2026-08-03 16:44:06 +07:00
asepharyana 24db0f19b1 ci: enable FlakeHub Cache (id-token: write + use-flakehub) 2026-08-03 16:20:15 +07:00
asepharyana ce5db6aa3c fix(media): normalize activeMode in backend MediaState (FE relies on it)
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.
2026-08-03 10:09:19 +07:00
asepharyana 44a0358b0c fix(voice): screen share restore is best-effort — Discord session teardown race
GoLive (dank074 Streamer) needs the single voice session; after the stream
ends, an automatic @discordjs/voice reconnect often races Discord's session
teardown and times out (AbortError). Restore is now best-effort with a 5s
delay; if it fails the FE shows disconnected and the user clicks Connect —
an accepted tradeoff for one-voice-session-per-user.
2026-08-03 09:33:29 +07:00
asepharyana d36c8777fe fix(voice): delay voice restore after screen share — avoid session teardown race
Immediate reconnect after Streamer.stop() races Discord's voice session
teardown → AbortError. Wait 4s so the old session is fully released before
re-joining with @discordjs/voice.
2026-08-03 09:21:10 +07:00
asepharyana 9fd4ded9c8 fix(voice): restore voice after screen share — pass pre-release status to callbacks
The restore callback previously read getVoiceStatus() AFTER disconnectGuild
had already cleared it, so it never knew which guild/channel to reconnect.
Now release/restore receive the status captured BEFORE the audio connection
is released, so reconnect actually happens after the GoLive stream ends.
2026-08-03 09:07:57 +07:00
asepharyana 55d28dc928 style(voice): biome format media handler + screen controller 2026-08-03 08:49:59 +07:00
asepharyana 02e2243a98 fix(voice): screen share releases audio connection so Streamer owns voice session
The dank074 Streamer creates its own WebRTC voice connection, but Discord
allows only ONE voice session per user. When VoiceController (audio) was
already connected, the Streamer join hung forever (never got
VOICE_SERVER_UPDATE). Now:

1. ScreenShareController takes releaseVoice/restoreVoice callbacks.
2. Before joining, it disconnects the @discordjs audio connection via
   VoiceController.disconnectGuild.
3. Streamer joins + streams GoLive.
4. After the stream ends, restoreVoice reconnects the audio connection so
   mic/listen keep working.
5. media.handler wires these via a new setVoiceController accessor from
   commandHandler; VoiceController is the single source of truth.

Also adds caller-bound timeouts & safe .catch() everywhere so a stream
failure can never become an unhandledRejection again.
2026-08-03 08:41:00 +07:00
asepharyana 8528f2c73d fix(voice): screen share join timeout — Streamer join hangs with dual voice conn 2026-08-03 08:19:05 +07:00
asepharyana 53f26185bc fix(voice): screen share crashed gateway — Streamer never joined voice + unhandledRejection
Root cause: ScreenShareController created @dank074 Streamer but never called
streamer.joinVoiceChannel() — playStream threw 'Bot is not connected to a
voice channel', and since the code only used .finally() (no .catch), the
rejection became an unhandledRejection that took down the whole gateway
(graceful shutdown triggered, systemd restarted).

Fixes:
1. Resolve active channel + streamer.joinVoiceChannel(channel) before
   prepareStream/playStream (dank074 needs its OWN WebRTC voice connection).
2. .catch() on the playStream done promise — log + kill ffmpeg instead of
   crashing the process.
3. .catch() on playback.done in media.handler too.
4. stop() now kills ffmpeg AND stops the streamer's voice connection.
2026-08-03 08:05:02 +07:00
asepharyana a57eeb2e22 fix(voice): media queue field mismatch, voice joinable filter, connect error toast
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.
2026-08-03 07:48:50 +07:00
asepharyana 9abb09dd33 feat(frontend): add light mode with runtime theme toggle
- globals.css: split theme tokens into :root (light default) + .dark
  overrides; switch @theme inline → @theme so utilities reference
  var(--color-*) and a runtime class swap actually re-skins the UI
  (inline inlines literal values and ignores .dark overrides)
- layout.tsx: drop the beforeInteractive inline theme script (it caused
  hydration instability); theme is applied client-side only
- top-nav: apply persisted theme on mount, default light, toggle
  updates <html> class + localStorage
- glass-intense: light variant (white card on light canvas); chart grid
  line uses var(--color-border); attachment chip uses bg-glass-bg
- Verified in static export: toggle dark↔light both directions,
  chatbot panel renders clean in both themes
2026-08-03 07:20:09 +07:00
asepharyana 831254bb71 fix(nix): filter build artifacts from frontend source
path: literals in flakes do NOT respect .gitignore, so a dirty local
out/ (stale chunks from previous builds, e.g. 3y39nidcm2n_s.js from
the removed quick-prompt button) leaked into the sandbox and got
served forever. Add filterSource helper that excludes out, .next,
node_modules, pnpm-lock.yaml from the frontend derivation source.
2026-08-03 06:53:46 +07:00
asepharyana 9718940258 fix(chatbot): FAB opens chat directly — remove extra toggle + UX polish
- chatbot-container: remove chatOpen layer — the minimized bubble now
  expands straight into the chat panel (single click, no extra button)
- Remove the redundant 'Tanya soal server...' quick-prompt button and
  the PanelLeft collapse toggle (one less state to fight)
- Bubble fixed h-[460px], chat panel flex-fills remaining space
- chat-panel: suggestion chips on empty state (suasana server, channel
  paling ramai, total pesan, pesan bermasalah) so first-time users can
  start with a single click
- Input: roomier padding, more descriptive placeholder, bigger send
  button, autoComplete off
- Drop chatOpen/setChatOpen from context (dead state)
2026-08-03 06:44:23 +07:00
asepharyana d1c1f3e4a7 feat(chatbot): per-user history via X-User-Id + agentic tools calling
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
2026-08-03 06:24:19 +07:00
asepharyana 7513681b4b feat(frontend): remove ugly canvas placeholder from chatbot
- Delete chatbot-canvas.tsx (placeholder face canvas) + its export
- Remove canvas area from chatbot container; chat panel gets the freed
  space (248px → 300px) and bubble height drops 440px → 400px
2026-08-03 06:11:06 +07:00
asepharyana 2b815e156c feat(frontend): rebuild chatbot UI to match backend — wider panel, guild context, Indonesian
- Chatbot bubble: 220px → 320px wide, 440px tall; proper header with
  drag handle; quick-prompt row when chat is closed
- ChatPanel: show ALL history (not last 8), Indonesian placeholder/empty
  state/error copy (backend speaks Indonesian), timestamps (id-ID),
  clear-history button, typing indicator bubbles, Enter-to-send
- Send active guildId as context so backend answers reference the real
  server (serverInsights path in chatbot.service)
- GuildId sync from layout → ChatbotProvider via ChatbotGuildSync
- chatbotApi.send(message, guildId) → POST /api/chat {message, context}
  matching BE zod schema (guildId optional)
2026-08-03 06:04:25 +07:00
asepharyana 03d59f0738 feat(frontend): lazy-load images, add lightbox viewer, polish AI panel
- Add loading=lazy + decoding=async to all <img> (message card preview,
  attachments grid, image grid, avatars via ui/avatar)
- New Lightbox component: fullscreen image viewer with keyboard nav
  (←/→/Esc), counter, click-to-close; wired into messages page + detail
- Attachments grid: click image to open, image counter badge, grouped
  non-image attachments
- AI analysis panel: line-clamp-3 with Show more/less toggle
- Message card: preview image is now a clickable button opening detail
2026-08-03 05:08:08 +07:00
asepharyana 5cc0f8a243 docs: frontend README dev note 2026-08-02 16:46:22 +07:00
asepharyana 12c55ef486 docs: sync remaining md (AGENTS, ARCHITECTURE, READMEs to 4001/4009, Nix) 2026-08-02 16:42:57 +07:00
asepharyana 38c27eb5bb chore: ARCHITECTURE.md DB pool example 2026-08-02 16:23:01 +07:00
asepharyana bd044e95c3 chore: env.test.example and ARCHITECTURE pool 6432 2026-08-02 16:22:48 +07:00
asepharyana ec64a078bf chore: fix-missing-tables.sql imrnes IP 2026-08-02 16:22:30 +07:00
asepharyana 37defa5915 chore: Dockerfile.backend expose port 4001 2026-08-02 16:21:48 +07:00
asepharyana 39421c39cb chore: sync port references and docs to 4000s infra 2026-08-02 16:20:27 +07:00
asepharyana 1d27f67788 chore: update gmw-proxy nginx template ports to 4009/4001 2026-08-02 15:16:16 +07:00
asepharyana d1e6f3b47a chore: update ports to 4000-range (4000/4001) 2026-08-02 14:30:54 +07:00
asepharyana dbcf9d68f2 fix(media): publish status when a track ends naturally
The Redis media:status key was only rewritten after a command received via
Redis. When the last track ended naturally (AudioPlayer Idle -> advanceQueue
with an empty queue), currentTrackItem was cleared but the status key was not
persisted — so the backend's cached status and the frontend's 10s polling
stayed stuck showing the finished track as 'playing' forever.

Wire a media-status sink (commandHandler provides the real redisPub to
MediaHandler) and re-publish status after auto-advance, so natural track end
updates the UI.
2026-08-02 10:43:34 +07:00
asepharyana ef4281cd1f fix(voice): activity tab rendered a permanently empty chart
VoiceActivityTimeline was never given a data prop — the Activity tab always
showed an empty Recharts bar chart while the connection tab already had live
speaker state. Replace the dead chart with a live speaker/activity list fed
from the same WebSocket data, so the tab reflects real state instead of
misleading empty bars.
2026-08-02 10:38:02 +07:00
asepharyana 25f6609a9f fix(recordings): stop faking duration from file size + render edited content in search
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.
2026-08-02 10:32:58 +07:00
asepharyana 3f199aa70d fix(messages): merge partial WS updates + display edited content
message_updated broadcasts only {id, edited_content, edited_at} (+ reset
ai_* fields), but the frontend replaced the whole cached record, wiping
username/content/channel_id/created_at -> blank cards and the
'the channel_id of undefined' crash on /messages. Merge partials over the
existing record (list + detail), make list-patching channel-filter aware,
show edited content/badge, and fix the message_updated WS type.

Also broadcast type:'edited' + ai reset in message_updated so the live UI
matches the DB update.
2026-08-02 10:27:41 +07:00
asepharyana a82265f4a9 chore: remove outdated README.md file 2026-08-02 10:18:56 +07:00
asepharyana 78d514b73d feat(dashboard): message activity timeline + moderation donut
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.
2026-08-01 23:06:00 +07:00
asepharyana 7d2bd75f6c ci: exclude e2e.test.ts from CI unit run (needs live backend API_BASE) 2026-08-01 22:21:55 +07:00
asepharyana b3a2f2ec10 ci: add test+typecheck gate before deploy
Sebelumnya CI hanya build nix -> deploy tanpa verifikasi — placeholder
tests sempat rusak berbulan-bulan tanpa terdeteksi. Job 'test' baru:
- pnpm install + tsc --noEmit + vitest run untuk backend & discord-gateway
- biome check (errors fail, warnings pass)
- build-and-deploy now needs: test
2026-08-01 22:10:28 +07:00
asepharyana 6293d588bc chore(lint): biome cleanup across services — format, sort imports, drop unused
- discord-gateway: 74 lint errors -> 0 (format, import sorting, unused
  imports/vars, dead breath var)
- backend: format + sort imports (11 warnings left: noExplicitAny)
- frontend: remove unused imports, drop dead breathing var, fix
  useExhaustiveDependencies (scroll keyed on messages), a11y biome-ignore
  for drag surface + stopPropagation container (mouse-only gestures)
- remaining warnings are false positives: index keys on static lists,
  <img> in static export (next/image unsupported), noExplicitAny

tsc --noEmit clean on all 3 services; vitest green (60+36).
2026-08-01 22:09:02 +07:00
asepharyana 2357421841 fix(test): repair placeholder tests referencing deleted @bete/shared package
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.
2026-08-01 22:08:54 +07:00
asepharyana 308be9f05a fix(nix): restrict flake to x86_64-linux (nixpkgs 26.11 dropped darwin) 2026-08-01 18:03:41 +07:00
asepharyana a0b3f7e9b2 ci: publish flake to FlakeHub (rolling) 2026-08-01 17:58:19 +07:00
asepharyana 98064d1dd9 feat(fe): recordings — visible playing/loading/paused states
- recording-card: kartu aktif di-highlight (ring primary + glow pulse),
  badge 'Now Playing'/'Loading'/'Paused', tombol play berubah jadi Pause
  saat playing dan spinner saat loading, waveform equalizer beranimasi
  (animate-eq, delay per bar) saat playing / pulse saat loading.
- recording-player: jadi now-playing panel — tombol play/pause + spinner
  loading, progress bar + waktu (current/duration), status 'loading…',
  audio element pindah ke sini + event onPlay/onPause/onWaiting/onCanPlay/
  onPlaying/onError naik ke page.
- recordings/page: state isPlaying/isLoadingAudio + audioRef, togglePlay
  (klik card lain = ganti track, klik card sama = pause/resume).
- globals.css: keyframes eq-bounce + card-glow.

Verified: FE tsc0, next build 10/10 static pages.
2026-08-01 16:54:58 +07:00
asepharyana 0daee56213 ci: migrate CI to GitHub Actions (deploy nix + mirror ke Gitea backup)
Mirror to Gitea / mirror (push) Successful in 26s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Failing after 32m40s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Failing after 18m0s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Failing after 18m0s
2026-08-01 16:40:27 +07:00
asepharyana 762e78d6b6 feat(fe): play Discord voice live + fix recording play/download
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m42s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 4m11s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 11m4s
Voice page (connection tab):
- ListenControl baru: toggle Listen (Headphones) — mulai PcmPlayer dari
  user gesture, subscribe onPcm WS, volume slider, bar level per-user
  REAL dari PCM (bukan random).
- lib/audio/pcm-player.ts (baru): ScriptProcessorNode mixer — ring buffer
  2s per user (hash FNV-1a sama dengan gateway), upsampling 24k→48k
  linear, mix semua user ke mono, gain volume, cleanup ring diam 5s.
- useVoiceListen + hashUserId di hooks; auto-stop saat disconnect.

Recordings:
- recording-player: reset src+load+play() eksplisit (bukan autoPlay doang),
  tampilkan filename + error state 'playback failed' kalau file rusak.
- recording-card: tombol Download fetch blob (CORS tele open) → objectURL
  → force download dengan nama asli; fallback buka tab baru kalau fetch
  gagal; spinner saat mendownload.

Verified: FE tsc 0, next build 10/10 static pages.
2026-08-01 16:30:56 +07:00
asepharyana 6ce784471e fix(gateway): repair Ogg page CRCs + deliver recordings as MP3
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m44s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 4m13s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 11m33s
Root cause rekaman gak bisa dibuka: prism-media OggLogicalBitstream
dipanggil dengan crc:false (node-crc dihapus dari dependency tree) —
semua page Ogg punya checksum 0 — player strict (ffmpeg, iOS) tolak
dengan 'CRC mismatch / End of file'. Rekaman 13:11 terbukti CRC-invalid.

Fix:
1. recorder/oggCrc.ts (baru): recompute CRC-32 (RFC3533, poly 0x04c11db7,
   initial 0, MSB-first) tiap page OggS in-place — pure JS tanpa native dep.
2. segmentFinalizer.ts: panggil fixOggCrc sebelum upload/merge.
3. recorder/uploader.ts: transcode segment ke MP3 (libmp3lame 128k 48k
   stereo) sebelum upload tele — universal playback. filename+size DB
   di-update; source OGG tetap untuk transkripsi.
4. muxer.ts + recorder.ts: merged session file juga .mp3.

Verified: ffprobe baca segmen yang tadinya CRC mismatch, MP3 valid.
2026-08-01 16:05:51 +07:00
asepharyana 0ef2b715c4 fix(gateway): enable stream for all LLM calls — router always streams SSE
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m36s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 4m9s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 11m29s
Audit lanjutan: 6x 'LLM API request failed: Request was aborted' per jam.
Root cause: 9router/omniroute SELALU balas SSE (data: chunks) walau request
tanpa stream:true — SDK OpenAI non-stream menunggu FULL body sebelum parse,
jadi batch moderasi besar yang upstream-nya lambat kena timeout 30-60s dan
di-abort. llmClient sudah punya agregasi streaming (chunks → ChatCompletion).

Fix: stream:true di llmCaller (moderasi batch/individual), llmVision,
cultureLearner, userProfileLearner. Verified: SDK stream test 806ms vs
sebelumnya abort. Caller lain (recovery worker dll) lewat llmCaller sama.
2026-08-01 15:00:59 +07:00
asepharyana dfe689bdec fix(gateway): mediaAnalysis ffprobe path, fallback error-log, generic closer sanitize
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m47s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 4m11s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 11m36s
Audit log produksi (sejak deploy13:38) menemukan 3 isu:
1. mediaDownloader.ts spawn /usr/bin/ffprobe + /usr/bin/ffmpeg (path keras) —
   ENOENT di Nix karena binary cuma di ffmpeg-headless closure. Pakai
   PATH-resolved ('ffprobe'/'ffmpeg') seperti voice-recording module
   (ffmpegProcess.ts/transmitter.ts) — 5 media warning hilang.
2. individualFallbackProcessor log error 'Success' di level50 tiap fallback
   BERHASIL (logModerationError dengan new Error('Success')) — ganti
   logger.info dengan verdict yang sama; error log cuma untuk error asli.
3. moderationResponseParser: strip frasa penutup generik ('Tidak ada
   indikasi pelanggaran.') yang masih sering dikeluarkan LLM walau prompt
   melarang (277/1486 analisis mengandung frasa, termasuk hari ini).
   sanitizeGenericCleanCloser hanya mencocok frasa di AKHIR, teks substantif
   tetap utuh. Unit test: 6/6 pass.
2026-08-01 14:00:43 +07:00
asepharyana ada7a768f8 fix(gateway): bump 0011 voice_transcription journal when above applied max
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 3m15s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m23s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 10m59s
Migration 0011 (add voice_recordings.transcription) when=1781388000000
lebih kecil dari 0010 (1781390000000) yang sudah ter-apply — drizzle
skip diam-diam (folderMillis <= max(created_at)), kolom transcription
tidak pernah dibuat. Recording OGG sukses tapi INSERT voice_recordings
gagal 42703 di produksi.

Fix: when=1785600000000 (> max applied 1785551832190) + apply manual
ALTER TABLE + insert row __drizzle_migrations dengan hash file yang
sama (c368acb0...) supaya gateway restart berikutnya skip (idempotent).
2026-08-01 13:15:03 +07:00
asepharyana 493bca590d fix(infra): build native voice deps (opus, datachannel) in Nix closure
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 4m21s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 4m22s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 11m31s
pnpm 11 rebuild hanya jalanin script package yang di-approve via
pnpm-workspace.yaml (allowBuilds) DAN abort di kegagalan pertama.
Yaml harus tracked (flake source cuma ikut file git). Tapi pnpm rebuild
tetap gagal karena: (1) node-crc MSRV cargo:: error — dead dep, dihapus
dari deps+patch; (2) sharp install script gagal di sandbox — binary-nya
prebuilt @img, script cuma validasi — dikeluarkan dari approval list;
(3) node-datachannel prebuild CLI TypeError + cmake-js butuh cwd benar.

Fix: loop eksplisit di buildPhase gateway yang jalankan install script
tiap native dep (opus/node-datachannel/zeromq) dengan cwd package dir,
+ cmake (dontUseCmakeConfigure biar stdenv nggak auto-configure),
+ opensslDevEnv = symlinkJoin pkgsStatic.openssl.out (libcrypto.a —
CMakeLists set OPENSSL_USE_STATIC_LIBS=TRUE; pkgs.openssl default
output = bin tanpa lib) + openssl.dev (headers),
+ git (libdatachannel FetchContent clone dari GitHub).

Verifikasi: result store punya opus.node (compile source), node_datachannel.node
(compile source), node-av prebuilt, zeromq prebuilt; runtime smoke test:
PeerConnection instantiate+close OK, OpusEncoder encode OK, dank074
Streamer/prepareStream/playStream load OK.
2026-08-01 12:44:01 +07:00
asepharyana 823b484497 chore(gateway): pnpm 11 build-script approvals (allowBuilds) for native voice deps
pnpm 11.17 mengabaikan field pnpm.onlyBuiltDependencies di package.json.
Native deps voice (@discordjs/opus, @lng2004/node-datachannel, zeromq, dll)
tidak pernah kebangun di Nix store karena flake pnpmInstall pakai
--ignore-scripts dan pnpm rebuild tanpa approval. Hasil: receiver/rekaman/
GoLive diam-diam tanpa decoder/encoder native.

pnpm approve-builds --all menulis allowBuilds:true per package di
pnpm-workspace.yaml (harus tracked — flake source cuma ikut file git).
node-crc tetap gagal build (MSRV cargo:: check) tapi tidak pernah
di-import di source — harmless.
2026-08-01 11:47:01 +07:00
asepharyana 891c1305f0 feat(gateway): restore Discord GoLive screenshare (dulu pernah ada, hilang saat split microservices)
User: 'dulu sharescreen juga bisa'. Terbukti: commit d50ce86 (Mei 2026)
punya src/media/screenShareController.ts + vendor @dank074/discord-video-stream,
hilang saat rombak monolith -> microservices. Interface ScreenShareController
masih ada di mediaTypes.ts tapi implementasinya tidak.

Restore:
- dep @dank074/discord-video-stream@6.0.0 (npm, dibangun untuk
  discord.js-selfbot-v13 — cocok dengan stack gateway)
- mediaSource.getDirectVideoUrl (yt-dlp --get-url bestvideo+bestaudio)
- screenShareController.ts (BARU): Streamer(client) + prepareStream H264
  720p30 + playStream go-live; owner check via discordPlayer
- media.handler: mode:'screen' di media:queue -> screen path; status
  expose activeMode; stop matiin screen
- FE: tombol Screen di MusicPlayer + hook useMediaQueue({url, mode})

Verifikasi: gateway tsc PASS, FE tsc PASS, biome 0 error, next build PASS.
Nix build pending (dep native @lng2004/node-datachannel butuh pnpm rebuild).
2026-08-01 11:36:40 +07:00
asepharyana 189ab1c1f6 feat(fe): real mic capture for voice transmit (kirim suara)
Sebelumnya tombol Live/Muted cuma kirim voice:transmit:start/stop ke
gateway — TIDAK ADA audio yang dikirim (0 getUserMedia/AudioContext di
frontend). MicControl cuma toggle state kosong.

- lib/audio/mic-transmit.ts (BARU): getUserMedia → AudioContext 48kHz →
  AudioWorklet (downsample 24kHz mono s16le + volume + chunk 20ms) →
  frame 'PCM\0' + Int16LE → ws.sendBinary. Worklet inline via Blob URL
  (aman untuk static export).
- useMicTransmit(ws): aktif = start capture + voice:transmit:start;
  nonaktif = stop capture + voice:transmit:stop; setVolume untuk slider.
- voice page: volume slider sekarang beneran ngatur gain mic; disconnect
  ikut matiin mic.

Verifikasi: tsc PASS, biome 0 error, next build PASS. Test mic butuh
real device (headless browser tidak punya mic) — protokol: connect voice
→ Live → ngomong → orang di channel denger.
2026-08-01 11:26:18 +07:00
asepharyana 9d60f00934 fix(infra): add ffmpeg + yt-dlp to gmw-discord-gateway runtime
Root cause voice tidak berfungsi di produksi: ffmpeg & yt-dlp cuma ada
di devShell, bukan di package discord-gateway. Bukti dari log gateway:
'FFmpeg/avconv not found!' saat voice:transmit:start (mic -> Discord),
yang juga mematikan music playback (StreamType.Arbitrary butuh ffmpeg)
dan segment muxing rekaman.

- buildInputs: pkgs.ffmpeg-headless + pkgs.yt-dlp
- wrapper export PATH ke keduanya sebelum exec node
Verifikasi: nix build PASS; closure berisi ffmpeg-8.1.2 + yt-dlp-2026.07.04;
wrapper PATH mengarah ke keduanya; ffmpeg/yt-dlp jalan.
2026-08-01 11:22:20 +07:00
asepharyana 903e7c4aeb feat(fe): remove Settings page from dashboard
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 3m7s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m28s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 3m28s
Hapus halaman /settings + nav item Settings (TopNav/HiddenSidebar
via navItems). GlassDivider ikut dihapus (cuma dipakai settings).
useConfig tetap (dipakai guild-selector).

Verifikasi: tsc PASS, biome 0 error, next build PASS (10 halaman
static, settings hilang).
2026-08-01 11:13:47 +07:00
asepharyana 0771e62223 feat(fe): remove Live tab from dashboard
Hapus tab Live + komponennya (LiveStream, ModQueue) dari halaman
dashboard — tab Stats/Users/Channels tetap. useReview tetap dipakai
messages page (review tab), jadi hook tidak dihapus.

Verifikasi: tsc PASS, biome 0 error, next build PASS.
2026-08-01 11:10:36 +07:00
asepharyana 8e7ba67248 fix: message detail shows only its own images, not everyone's
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m47s
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 3m12s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m12s
useMessageDetail fetch attachments per-channel -> detail view nampilin
10 image terbaru di channel itu (semua orang), bukan image pesan yang
diklik. Root cause: getAttachmentsByChannel cuma filter channel_id.

- backend: messageQuerySchema + messageId optional; repository
  tambah eq(message_id) pas messageId ada. Sudah ada index
  idx_attachments_message.
- frontend: getAttachments(channelId, limit, cursor, messageId);
  useMessageDetail pass id pesan.

Verifikasi: backend tsc PASS, frontend tsc PASS + next build PASS.
E2E local (tsx + env prod, port 3901): tanpa messageId -> 3 row
(termasuk image.png punya message lain); dengan messageId -> tepat
2 row milik pesan itu. psql juga konfirmasi channel-only 3 vs
message-filtered 2.
2026-08-01 10:59:54 +07:00
asepharyana 8a4024f619 fix(fe): crash 'reading charAt' on message avatar fallback
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 2m47s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 3m2s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m19s
msg.username bisa undefined saat pesan live masuk lewat WS
(message_updated Partial payload / capture tidak lengkap) ->
msg.username.charAt(0) TypeError, halaman /messages mati.

- message-card + search-panel: username?.charAt(0) ?? '?'
- created_at di-guard juga biar tidak render 'Invalid Date'
Verifikasi: tsc PASS, build PASS, biome 0 error. DB saat ini 0 row
null username (1176 total) — crash murni dari jalur WS live.
2026-08-01 10:37:21 +07:00
asepharyana ec89d64dbf fix(fe): crash 'reading channel_id' + recharts width/height warnings
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 2m14s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m53s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m15s
- useMessageDetail: guard attachments fetcher — revalidate bisa race
  detail load, detail.data undefined saat fetcher jalan ->
  TypeError 'Cannot read properties of undefined (reading channel_id)'
  yang bikin halaman /messages mati (Next error boundary). Sekarang
  fetcher balikin [] kalau channel_id belum ada; hapus non-null
  assertion. Diverifikasi: /messages sebelumnya crash, sekarang render
  dengan data asli (list, verdict, confidence, sticker, emoji).
- ResponsiveContainer (recharts 3.8): initialDimension -1 di render
  pertama -> warning 'width(-1) and height(-1)'. Pakai height numerik
  tetap (192/160/48) + minWidth/minHeight 0 -> calculatedHeight >0,
  warning hilang; width tetap responsif via ResizeObserver. Chart
  baru dirender setelah mount (useMounted) biar container punya ukuran.
  Diverifikasi console: 0 warning, 0 error di dashboard & voice.
2026-08-01 10:21:23 +07:00
asepharyana 6f41c22cb5 fix(db): migration 0013 rename mascot_chat_messages to chatbot_messages
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 2m17s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 2m59s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m7s
/api/chat/history 500 (relation "chatbot_messages" does not exist):
codebase di-rename mascot->chatbot (977a6f9) tapi tabel DB tidak
pernah di-migrate — backend SELECT dari chatbot_messages, DB masih
mascot_chat_messages dengan kolom mascot_response.

- Migration 0013 (idempotent): ALTER TABLE mascot_chat_messages RENAME
  TO chatbot_messages, RENAME COLUMN mascot_response -> bot_response,
  RENAME INDEX -> idx_chatbot_messages_user_created; journal when >
  max(created_at) di __drizzle_migrations (0012)
- Diterapkan live via psql; riwayat chat lama tetap ada
- Verifikasi: GET /api/chat/history 200 + data, POST /api/chat 200
  + tersimpan (total history 1 -> 2)
2026-08-01 09:38:27 +07:00
asepharyana 01c18b2060 refactor(fe): replace TanStack Query with SWR + UI/data cleanup
Build & Deploy (Nix) / build-and-deploy (backend) (push) Successful in 2m56s
Build & Deploy (Nix) / build-and-deploy (discord-gateway) (push) Successful in 3m42s
Build & Deploy (Nix) / build-and-deploy (proxy) (push) Successful in 3m59s
Rombak data layer frontend:
- Hapus @tanstack/react-query (package.json, lockfile, provider di
  dashboard layout) — ganti SWR 2.4.2 + SWRConfig (revalidateOnFocus
  false, deduping 10s, no retry on 404)
- Semua hooks data ditulis ulang ke useSWR; useAction() helper baru
  pengganti useMutation dengan surface kompatibel (mutate/mutateAsync/
  isPending/error)
- useMessages + useMessagesHasMore share satu SWR key — probe cursor
  yang tadinya dobel fetch API sekarang deduped
- WS sync (messages/media/recordings) pindah dari queryClient ke
  SWR mutate dengan filter key + revalidate:false
- useMessageSearch() dipakai search-panel & search-overlay; search
  overlay backdrop div -> button (fix a11y lint)

Rapikan UI + isi data:
- Tab stats recordings: placeholder 'coming soon' diganti stat asli
  (total, ukuran, speaker unik, top speakers)
- Empty states konsisten via EmptyState (images/review/recordings),
  EmptyState terima className
- biome check --write: 0 error, 8 warning pre-existing
- Verifikasi: tsc --noEmit PASS, next build PASS (11 halaman static),
  API live dicek — semua endpoint dashboard/messages/guilds/config/
  voice/media/recordings/review balikin data
2026-08-01 09:19:39 +07:00
asepharyana 1f91f99de3 feat(automod): render sticker, role & user names in moderation views
QoL lanjutan dari fix60084b3: content pesan mentah masih nampilin
snowflake (<@&roleid>, <@userid>, <:emoji:id>) di log moderasi dan
prompt LLM. Sekarang dirender ke nama yang bisa dibaca:

- Gateway capture: metadata menyimpan mentionedRoles + mentionedUsers
  (id+name) dari message.mentions, disimpan ke metadata JSON
- renderDiscordMentions(): <@&id> -> @RoleName, <@id> -> @Username,
  <:name:id> -> :name:, fallback @role/@user — dipakai di
  conversationContext (konteks LLM) dan moderationBuilders
  (getAnalysisContent) sehingga LLM lihat nama role/user beneran,
  bukan placeholder generik
- Frontend renderMessageContent() (mirror gateway) dipasang di semua
  tempat nampilin content: message-card, message-detail(-view),
  search-overlay, search-panel, users/channels section, live-stream,
  mod-queue, review list; sticker-only message tetap [Sticker: name],
  pesan teks+sticker kini ikut nampilin nama sticker
- tsc --noEmit PASS di gateway & frontend; renderDiscordMentions
  diverifikasi manual (6 kasus: role/user/emoji/unknown/plain)
2026-08-01 08:56:10 +07:00
asepharyana 9ba2262446 refactor: improve image handling and accessibility in MessagesPage
Deploy to VPS / deploy (push) Successful in 2m17s
2026-07-26 17:31:44 +07:00
asepharyana eca0a378f9 fix: increase max length for user profile summary and sanitize function
Deploy to VPS / deploy (push) Successful in 2m19s
2026-07-26 17:27:51 +07:00
asepharyana b886b71bad feat: add user detail and selection functionality to dashboard
Deploy to VPS / deploy (push) Successful in 2m23s
2026-07-26 17:15:08 +07:00
asepharyana 05bbae66da Refactor voice page and hooks to use React Query for data fetching
Deploy to VPS / deploy (push) Successful in 2m57s
- Updated VoicePage component to utilize useVoiceConnect, useVoiceDisconnect, and useMicTransmit mutations.
- Replaced local state management with React Query's useQuery for voice status, guilds, and channels.
- Removed custom useAsync hook and replaced it with useQuery in useConfig, useStats, useUsers, useChannels, and useMessages hooks.
- Simplified useRecordings and useSpeakers hooks to leverage React Query for data fetching and mutations.
- Removed deprecated use-async hook and related code.
- Enhanced error handling and loading states across various hooks.
2026-07-26 16:55:20 +07:00
asepharyana 323fa207af refactor: streamline data fetching in various hooks and components
Deploy to VPS / deploy (push) Successful in 2m39s
2026-07-26 16:46:53 +07:00
asepharyana 1021301656 fix: add error logging for various API calls across components
Deploy to VPS / deploy (push) Successful in 2m20s
2026-07-26 16:34:06 +07:00
asepharyana 74eabb7231 feat: enhance guild selection logic with auto-selection based on config
Deploy to VPS / deploy (push) Successful in 2m25s
2026-07-26 16:25:04 +07:00
asepharyana 39a3bc6269 fix: add missing draining state variable in VoiceTransmitter class
Deploy to VPS / deploy (push) Successful in 2m18s
2026-07-26 16:18:33 +07:00
asepharyana d5a547eb25 feat: add app header, sidebar, and mobile navigation components
Deploy to VPS / deploy (push) Failing after 1m45s
- Implemented AppHeader component with theme toggle and connection status.
- Created AppSidebar component for navigation with connection status indicator.
- Added MobileNav component for mobile navigation with responsive design.
- Introduced shared components: DetailStat, EmptyState, ErrorState, LoadingSkeleton, and StatCard for consistent UI.
- Developed hooks for async data fetching: useAsync, useConfig, useDashboard, useGuilds, useMedia, useMessages, useRecordings, and useVoice.
- Added chatbot API functions for sending messages and managing chat history.
2026-07-26 16:14:32 +07:00
asepharyana 726ea8fca5 fix: improve readability of download URL handling in RecordingsPage
Deploy to VPS / deploy (push) Successful in 2m7s
2026-07-26 15:45:29 +07:00
asepharyana 779fb6b338 fix: prevent opening undefined download URLs in RecordingsPage
Deploy to VPS / deploy (push) Failing after 1m31s
2026-07-26 15:43:10 +07:00
asepharyana 300512b2cb refactor: clean up code formatting and improve consistency across multiple components
Deploy to VPS / deploy (push) Failing after 1m59s
2026-07-26 15:39:41 +07:00
asepharyana c2502a0e5f feat: add recordings, settings, and voice pages with WebSocket integration
Deploy to VPS / deploy (push) Failing after 1m36s
- Implemented RecordingsPage to display and manage voice recordings with live updates via WebSocket.
- Created SettingsPage for user preferences, including theme toggling and server configuration display.
- Developed VoicePage for managing voice connections, including guild and channel selection, and active speaker display.
- Introduced GuildSelector component for selecting Discord guilds with error handling and loading states.
- Added utility functions for formatting numbers and bytes, and safely parsing JSON.
- Established navigation structure for the dashboard with relevant links for new features.
2026-07-26 15:34:08 +07:00
asepharyana eae0d7ce56 feat: enhance MessagesPanel with improved UI components and functionality
Deploy to VPS / deploy (push) Successful in 2m13s
- Refactored MessagesPanel to utilize new UI components such as Avatar, Badge, Button, Card, Dialog, Input, Progress, ScrollArea, Select, Skeleton, and Tabs.
- Improved error handling and loading states with enhanced user feedback.
- Updated message rendering logic to support new design patterns and animations.
- Added support for image previews and improved layout for message details.
- Enhanced mobile responsiveness with useIsMobile hook adjustments.
- Cleaned up utility functions for better readability and consistency.
2026-07-26 15:13:18 +07:00
asepharyana f57a1caf62 refactor: standardize import statements and improve code consistency across components
Deploy to VPS / deploy (push) Failing after 1m35s
- Updated import statements to use consistent semicolon usage.
- Refactored component code to ensure consistent formatting and style.
- Improved readability by adding missing semicolons and adjusting spacing.
- Ensured all components follow the same structure for props and return statements.
2026-07-26 14:52:38 +07:00
asepharyana e2c2365208 feat: add UI components including Skeleton, Slider, Sonner, Spinner, Switch, Table, Tabs, Textarea, Toggle Group, Toggle, Tooltip, and custom hook for mobile detection 2026-07-26 14:45:47 +07:00