- 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.)
- Add pgCorrectedModerationsTable to shared schema
- Create backend corrections module (stats, list, create endpoints)
- Add TunerPanel with Stats, History, and Submit sub-tabs
- Add AuthOverlay gate for Tuner (admin-only, same as Live)
- Set messages as default tab
- Wire Tuner into sidebar, header, and mobile tab bar
Co-Authored-By: Claude <noreply@anthropic.com>
This commit introduces several significant improvements across the backend and gateway services:
- **Data Retention**: Added an automated cleanup scheduler in `discord-gateway` to prune expired messages, attachments, and voice recordings based on configurable retention policies.
- **Observability**: Integrated `prom-client` in the `backend` service to expose Prometheus metrics via `/api/metrics` and added default Node.js runtime metrics.
- **Media Handling**: Enhanced `MediaHandler` in `discord-gateway` to support media URL resolution and improved playback status tracking.
- **API & Config**: Expanded the configuration endpoint to expose more system settings and reorganized `.env.example` for better readability.
- **Refactoring & Cleanup**:
- Removed unused `better-sqlite3` dependency.
- Refactored voice channel routing.
- Improved error handling and testing coverage with comprehensive unit tests for shared utilities and error classes.
- **Documentation**: Added `MEMORY.md` for project context.
Refactor the voice recording and playback systems to improve efficiency, reduce latency, and enhance Docker build performance.
- **Infrastructure**: Optimize Dockerfiles using build mounts for pnpm cache and reorder layers for better caching of dependencies and build tools.
- **Backend/Gateway**:
- Refactor `broadcast` module to use a generic event-based system instead of hardcoded functions.
- Simplify voice recording logic by merging metadata and segment management into a unified `segment.ts`.
- Optimize audio downsampling in `streamSetup.ts` using `Int16Array` views for better performance.
- Implement `withFallback` utility for more robust Redis/Database command execution.
- **Frontend**:
- Optimize audio playback visualization using pre-computed level shapes and efficient RMS calculation.
- Reduce latency in voice commands by prioritizing WebSocket communication over HTTP.
- Improve base64 encoding efficiency in audio transmission.
- **General**:
- Add default value for `ADMIN_PASSWORD` in shared config.
- Fix Docker healthcheck to use `127.0.0.1` instead of `localhost`.
- Shared Redis channel constants as single source of truth (redis-channels.ts)
- commandHandler.ts split into VoiceHandler, MediaHandler, GuildHandler,
ModerationHandler with handler-registry.ts dispatch
- messageStore.ts (1322 lines) split into domain-specific DB files:
messages.db.ts, attachments.db.ts, reviews.db.ts,
moderation-actions.db.ts, retention.db.ts
- recorder.ts startSpeaking callback extracted into speakingHandler.ts,
streamSetup.ts, segmentFinalizer.ts
- autoDeleteManager.ts split into autoDeleteEligibility.ts,
autoDeleteNotify.ts, autoDeleteLogger.ts
- Added createChildLogger() logging across 8 service files
- Backend messages.repository.ts migrated from raw SQL to Drizzle ORM
- Fixed biome.json to exclude packages/**/dist/* from lint
- Fixed config.ts GUILD_ID pre-existing type error
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Improve system reliability and real-time capabilities by implementing a robust lifecycle management system and adding new broadcast events for attachments and voice recordings.
- Implement asynchronous graceful shutdown in backend to close HTTP, WebSocket, Redis, and database connections.
- Add new WebSocket broadcast events: `attachment_created`, `voice_recording_started`, `voice_recording_stopped`, `voice_recording_uploaded`, and `analysis_queue_status`.
- Refactor media status handling to use boolean `playing` state instead of string-based status.
- Centralize `PageResult` and `VoiceRecording` types to improve consistency between frontend and backend.
- Update frontend API client to include `listRecordings` and handle new WebSocket event types.
- Fix type mismatches in voice command handling and media status reporting.
Prevents vitest from exiting with code 1 when no test files exist.
Adds minimal test files to backend and discord-gateway services.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Consolidate eventTypes.ts as single source of truth for Redis channels:
- Remove duplicate DiscordGatewayEvent interface from eventBroadcaster.ts
- Replace all hardcoded channel strings with EventChannels constants
- eventTypes.ts is no longer an orphan file
- Remove dangerous moderation action feature (selfbot safety):
- Remove /messages/:id/moderate endpoint from backend
- Remove moderation:action handler from commandHandler.ts
- Remove publishFireAndForgetCommand (wrong envelope format)
- Verified no remaining references to moderation:action in code
- Remove unused getCommandPublisher import from redis-bridge.ts
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Standardize MessageRecord types — single source of truth from @bete/shared
- Clean up config: remove unused GUILD_ID/TEXT_GUILD_ID/TEXT_CHANNEL_ID, fix WEBSERVER_PORT default (3001), remove default admin password
- Move mascot_chat_messages table to Drizzle schema with proper migration
- Remove runtime DDL (CREATE TABLE IF NOT EXISTS) from mascot-chat repository
- Remove phantom analytics/ module from documentation
- Add better-sqlite3 dependency to root devDependencies
- Replace 'as any' casts with proper type assertions across AI moderation
- Add error logging to silent catch blocks in LLM client
- Apply Biome formatting and import organization
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate configuration validation and core moderation types from individual services to the `@bete/shared` package to ensure consistency across the monorepo.
- Move `AppConfig` and moderation-related interfaces to `packages/shared`.
- Replace service-specific Zod schemas with the centralized shared configuration.
- Refactor `services/backend` and `services/discord-gateway` to consume shared config and types.
- Remove redundant type definitions and local configuration logic in services.
- Update `packages/shared` exports to include new `config` and `moderation-types` modules.
- Clean up unused files and deprecated utility functions in `packages/shared`.
- fix(backend): replace raw .parse() with proper loadConfig() + ConfigError
- fix(gateway): connect voice recording uploader to EventBroadcaster
- fix(gateway): remove dead globalThis.moderationBroadcaster path in AI analyzer
- fix(gateway): eliminate audioStream race condition by attaching handlers before pipe
- fix(gateway): enable inlineVolume by default for setMusicVolume to work
- fix(gateway): reuse persistent redisPub for command replies (no new connection per cmd)
- fix(frontend): add missing voice_active_user/voice_pcm_data to WsEventMap
- fix(frontend): correct onAttachmentUploaded handler signature to accept data
- chore: move @types/pg from dependencies to devDependencies
- chore: translate remaining Indonesian comments to English
- chore: remove stale P3 TODO comment
Route was POST /api/command (mounted at /api + /command)
Fixed to POST /api/voice/command (mounted at /api + /voice/command)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add POST /api/voice/command endpoint in backend
- Frontend transmit now sends commands via dedicated WebSocket connection
with HTTP API fallback if WebSocket fails
- Fixes issue where transmit start command was never received by discord-gateway
Previously commands were sent via the existing dashboard WebSocket,
which may not be connected when the Transmit button is pressed.
Now each command opens a fresh WebSocket connection with HTTP fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add discord:voice:pcm to SUBSCRIPTIONS as regular JSON channel
- Remove BINARY_CHANNELS and handleBinaryMessage function
- Discord-gateway sends PCM as JSON with base64, not raw binary
- This fixes PCM data not reaching browser via WebSocket
The issue was backend expected binary format but gateway sends:
{"type":"voice_pcm_data","data":{"userId":"...","pcm":"base64..."}}
Now backend correctly subscribes and broadcasts this to WebSocket clients.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements full audit from double_queue_audit.md.
## Critical
fix(backend): reanalyze-batch — per-scope in-flight guard (messages.routes.ts)
Two concurrent admin sessions clicking 'Retry All Errors' simultaneously now
get a 409 REANALYZE_BATCH_IN_PROGRESS for the same guildId:channelId scope.
Prevents the recovery worker from being triggered twice for the same set of
error messages.
fix(discord-gateway): messageUpdate embed resolution skip (messageCapture.ts)
Discord fires messageUpdate when link previews resolve 1-2s after send
even though the message body is unchanged. Compare newContent vs
existingContent before resetting ai_status to pending and re-queueing LLM.
Eliminates a spurious duplicate analysis that could overwrite a valid result.
## High
fix(discord-gateway): scheduleAutoDelete idempotency (aiAnalyzer.ts)
Add autoDeleteInFlight Set. Both processBatch and processIndividualFallback
call scheduleAutoDelete; without the guard, a message that races through
both paths launches two concurrent attemptAutoDeleteFlaggedMessage calls,
producing a duplicate moderation-action log entry and a Discord 10008 error.
The Set is cleaned up in a .finally() block after each attempt completes.
## Medium
fix(frontend): revert optimistic pending state on HTTP failure (useMessages.ts)
Capture the prior MessageRecord inside the setMessages functional updater
(no extra useCallback deps needed). If reanalyzeMessage() throws, restore
the snapshot so the UI reflects the real DB state instead of lying.
fix(discord-gateway): exclude individual_analysis_exhausted from recovery queries
Both getConversationKeysWithIncompleteAnalysis and
getIncompleteMessagesByConversation now add a NOT LIKE guard for
individual_analysis_exhausted. Prevents an infinite recovery loop if a bug
ever writes both flags to the same row.
## Low
fix(discord-gateway): unified timer path in scheduleConversationAnalysis
Remove the separate cooldown-path that created a secondary timer calling
scheduleConversationAnalysis recursively. Replace with a single
clear-and-reset pattern where delayMs = max(cooldownRemainder+500, debounce).
Eliminates the edge case where both timers were live simultaneously.
## Misc
fix(backend): cast req.params.id to String() to satisfy Express typings
Pre-existing tsc error (string | string[]) exposed by our edit.
String() is correct; route params are always scalar strings at runtime.
Three root causes patched:
1. aiAnalyzer.ts — processBatch apiFailedMessages path:
After reverting messages to 'pending', suppress shouldScheduleNext
(was true by default) to prevent scheduleConversationAnalysis from
firing immediately and racing with the recovery worker that will pick
up those same pending messages on its next poll cycle.
Also release the conversationProcessing lock immediately after the
revert so the cooldown timer (not the full processing-timeout) gates
the next attempt.
2. messages.routes.ts — POST /messages/:id/reanalyze:
Add a per-message reanalyzeInFlight Set. Concurrent requests for
the same ID now return HTTP 409 instead of issuing duplicate UPDATEs
and triggering multiple recovery worker activations.
Also narrow the SQL predicate to 'WHERE id = AND ai_status != pending'
so a click that arrives while the recovery worker already picked the
message up is a no-op at the DB level.
Excludes image, video, audio, and document file extensions from the
topic word extraction process to improve the quality of generated
analytics topics.
Replaces the previous AI-category based topic analysis with a word-based
frequency analysis using a Common Table Expression (CTE).
- Implements `word_list` CTE to split message content into individual words.
- Adds regex filtering to exclude URLs, Discord stickers, and emojis.
- Implements a stop-word filter to remove common Indonesian and English
conjunctions, pronouns, and prepositions.
- Filters out words shorter than 3 characters.
- Updates the aggregation to group by word and limit results to the top 10.
Refactor the mascot chat service to use an LLM instead of hardcoded
rule-based responses. This includes building system prompts from
server insights and constructing conversation history for context-aware
interactions.