refactor: large codebase cleanup - consolidate schemas, migrate to Drizzle ORM, extract frontend components, modernize Docker builds
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>
This commit is contained in:
Developer
2026-07-27 21:54:31 +07:00
co-authored by Claude Opus 4.8
parent 63f21513bd
commit 5802d02e29
223 changed files with 11499 additions and 13350 deletions
+8
View File
@@ -0,0 +1,8 @@
export {
broadcastBinary,
broadcastEvent,
clearBroadcastFunctions,
setBroadcastFunctions,
} from "./broadcast.js";
export { startRedisBridge, stopRedisBridge } from "./redis-bridge.js";
export { closeWebSocketServer, createWebSocketServer } from "./server.js";
+11 -69
View File
@@ -1,27 +1,4 @@
import {
DISCORD_ANALYSIS_QUEUE_STATUS,
DISCORD_ATTACHMENT_CREATED,
DISCORD_ATTACHMENT_UPLOADED,
DISCORD_CHANNEL_TOPIC_UPDATED,
DISCORD_GUILD_MEMBER_ADDED,
DISCORD_GUILD_MEMBER_REMOVED,
DISCORD_MESSAGE_ANALYZED,
DISCORD_MESSAGE_CREATED,
DISCORD_MESSAGE_DELETED,
DISCORD_MESSAGE_UPDATED,
DISCORD_PRESENCE_UPDATED,
DISCORD_REACTION_ADDED,
DISCORD_REACTION_REMOVED,
DISCORD_THREAD_CREATED,
DISCORD_THREAD_DELETED,
DISCORD_THREAD_UPDATED,
DISCORD_VOICE_ACTIVE_USER,
DISCORD_VOICE_ANALYZED,
DISCORD_VOICE_PCM,
DISCORD_VOICE_STARTED,
DISCORD_VOICE_STOPPED,
DISCORD_VOICE_UPLOADED,
} from "@bete/shared";
import { DISCORD_CHANNEL_TO_WS_EVENT, DISCORD_VOICE_PCM } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import Redis from "ioredis";
import { config } from "../shared/config/index.js";
@@ -29,41 +6,8 @@ import { broadcastBinary, broadcastEvent } from "./broadcast.js";
const logger = createChildLogger("ws.redis-bridge");
interface ChannelMapping {
channel: string;
eventType: string;
}
const SUBSCRIPTIONS: ChannelMapping[] = [
{ channel: DISCORD_MESSAGE_CREATED, eventType: "message_created" },
{ channel: DISCORD_MESSAGE_UPDATED, eventType: "message_updated" },
{ channel: DISCORD_MESSAGE_DELETED, eventType: "message_deleted" },
{ channel: DISCORD_MESSAGE_ANALYZED, eventType: "message_analyzed" },
{ channel: DISCORD_ATTACHMENT_CREATED, eventType: "attachment_created" },
{ channel: DISCORD_ATTACHMENT_UPLOADED, eventType: "attachment_uploaded" },
{ channel: DISCORD_VOICE_STARTED, eventType: "voice_recording_started" },
{ channel: DISCORD_VOICE_STOPPED, eventType: "voice_recording_stopped" },
{ channel: DISCORD_VOICE_UPLOADED, eventType: "voice_recording_uploaded" },
{
channel: DISCORD_ANALYSIS_QUEUE_STATUS,
eventType: "analysis_queue_status",
},
{ channel: DISCORD_VOICE_ACTIVE_USER, eventType: "voice_active_user" },
{ channel: DISCORD_VOICE_PCM, eventType: "voice_pcm_data" },
{ channel: DISCORD_VOICE_ANALYZED, eventType: "voice_analyzed" },
{ channel: DISCORD_REACTION_ADDED, eventType: "reaction_added" },
{ channel: DISCORD_REACTION_REMOVED, eventType: "reaction_removed" },
{ channel: DISCORD_THREAD_CREATED, eventType: "thread_created" },
{ channel: DISCORD_THREAD_DELETED, eventType: "thread_deleted" },
{ channel: DISCORD_THREAD_UPDATED, eventType: "thread_updated" },
{
channel: DISCORD_CHANNEL_TOPIC_UPDATED,
eventType: "channel_topic_updated",
},
{ channel: DISCORD_PRESENCE_UPDATED, eventType: "presence_updated" },
{ channel: DISCORD_GUILD_MEMBER_ADDED, eventType: "guild_member_added" },
{ channel: DISCORD_GUILD_MEMBER_REMOVED, eventType: "guild_member_removed" },
];
/** Channels we subscribe to = all keys in DISCORD_CHANNEL_TO_WS_EVENT */
const SUBSCRIPTION_CHANNELS = Object.keys(DISCORD_CHANNEL_TO_WS_EVENT);
let subscriber: Redis | null = null;
@@ -72,8 +16,8 @@ function createSubscriber(): Redis {
}
function handleSubscriptionMessage(channel: string, message: string): void {
const mapping = SUBSCRIPTIONS.find((m) => m.channel === channel);
if (!mapping) {
const eventType = DISCORD_CHANNEL_TO_WS_EVENT[channel];
if (!eventType) {
logger.warn({ channel }, "Received message for unmapped Redis channel");
return;
}
@@ -97,7 +41,7 @@ function handleSubscriptionMessage(channel: string, message: string): void {
const data = envelope.data !== undefined ? envelope.data : envelope;
// Voice PCM: decode base64 → binary broadcast instead of JSON
if (mapping.eventType === "voice_pcm_data") {
if (channel === DISCORD_VOICE_PCM) {
const pcmPayload = data as { userId?: string; pcm?: string };
if (pcmPayload?.pcm && pcmPayload?.userId) {
try {
@@ -115,11 +59,8 @@ function handleSubscriptionMessage(channel: string, message: string): void {
}
}
logger.debug(
{ channel, eventType: mapping.eventType },
"Broadcasting Redis event",
);
broadcastEvent(mapping.eventType, data);
logger.debug({ channel, eventType }, "Broadcasting Redis event");
broadcastEvent(eventType, data);
}
/** Simple 32-bit FNV-1a hash for userId → 4-byte identifier */
@@ -162,7 +103,7 @@ export async function startRedisBridge(): Promise<void> {
await subscriber.ping();
logger.info("Redis ping OK");
const channels = SUBSCRIPTIONS.map((m) => m.channel);
const channels = SUBSCRIPTION_CHANNELS;
await subscriber.subscribe(...channels);
logger.info({ channels }, "Subscribed to Redis channels");
@@ -184,8 +125,9 @@ export async function stopRedisBridge(): Promise<void> {
logger.info("Redis bridge stopped");
} catch (err) {
logger.error({ err }, "Error stopping Redis bridge");
} finally {
// Force-close on error
subscriber.disconnect();
} finally {
subscriber = null;
}
}
+47 -60
View File
@@ -13,9 +13,21 @@ interface BroadcastEvent {
timestamp: string;
}
interface JsonMessage {
type: string;
buffer?: string;
command?: string;
payload?: Record<string, unknown>;
}
// Track the active WebSocket server for lifecycle management
let _wss: WebSocketServer | null = null;
type MessageHandler = (
ws: WebSocket,
message: JsonMessage,
) => Promise<void> | void;
async function sendInitialStates(ws: WebSocket): Promise<void> {
// Send initial user state
ws.send(
@@ -71,6 +83,35 @@ export function createWebSocketServer(server: Server): WebSocketServer {
const wss = new WebSocketServer({ server, path: "/ws" });
_wss = wss;
// Map-based dispatcher for JSON WebSocket message types
const jsonHandlers = new Map<string, MessageHandler>();
jsonHandlers.set("voice_transmit", async (_ws, message) => {
if (!message.buffer) return;
const { getCommandPublisher } = await import("../shared/redis/index.js");
const publisher = getCommandPublisher();
await publisher.publish(
BACKEND_VOICE_TRANSMIT,
JSON.stringify({ type: "pcm", buffer: message.buffer }),
);
});
jsonHandlers.set("voice_command", async (_ws, message) => {
if (!message.command) return;
const { getCommandPublisher } = await import("../shared/redis/index.js");
const publisher = getCommandPublisher();
const commandId = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
await publisher.publish(
BACKEND_COMMAND,
JSON.stringify({
id: commandId,
type: message.command,
payload: message.payload ?? {},
replyChannel: `reply:${commandId}`,
}),
);
});
wss.on("connection", (ws: WebSocket, req) => {
// Parse auth token from query string
const rawUrl = req.url ?? "/";
@@ -103,7 +144,7 @@ export function createWebSocketServer(server: Server): WebSocketServer {
ws.on("message", (data: Buffer) => {
// Gateway PCM forward — broadcast raw binary to frontend clients only
if (isGateway && Buffer.isBuffer(data)) {
broadcastBinaryToFrontend(data);
broadcastBinary(data);
return;
}
@@ -146,52 +187,11 @@ export function createWebSocketServer(server: Server): WebSocketServer {
) {
try {
const message = JSON.parse(data.toString());
if (message.type === "voice_transmit" && message.buffer) {
// Legacy: Forward PCM data to Redis for discord-gateway
import("../shared/redis/index.js").then(
({ getCommandPublisher }) => {
const publisher = getCommandPublisher();
publisher
.publish(
BACKEND_VOICE_TRANSMIT,
JSON.stringify({
type: "pcm",
buffer: message.buffer,
}),
)
.catch((err: Error) => {
logger.error(
{ err },
"Failed to publish voice transmit to Redis",
);
});
},
);
} else if (message.type === "voice_command" && message.command) {
// Forward voice commands to discord-gateway with payload
import("../shared/redis/index.js").then(
({ getCommandPublisher }) => {
const publisher = getCommandPublisher();
const commandId = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
publisher
.publish(
BACKEND_COMMAND,
JSON.stringify({
id: commandId,
type: message.command,
payload: message.payload ?? {},
replyChannel: `reply:${commandId}`,
}),
)
.catch((err: Error) => {
logger.error(
{ err },
"Failed to publish voice command to Redis",
);
});
},
);
const handler = jsonHandlers.get(message.type);
if (handler) {
Promise.resolve(handler(ws, message)).catch((err: Error) => {
logger.error({ err }, "JSON message handler failed");
});
}
} catch (err) {
logger.debug({ err }, "Failed to parse WebSocket message as JSON");
@@ -234,19 +234,6 @@ export function createWebSocketServer(server: Server): WebSocketServer {
// Don't let the interval keep the process alive after wss closes
heartbeatInterval.unref();
// Forward gateway binary to frontend clients (no loopback to gateway)
function broadcastBinaryToFrontend(data: Buffer) {
for (const client of frontendClients) {
if (client.readyState === WebSocket.OPEN) {
try {
client.send(data);
} catch (err) {
logger.error({ err }, "Failed to send binary to frontend client");
}
}
}
}
// JSON event broadcast — frontend clients only
function broadcast(event: Omit<BroadcastEvent, "timestamp">) {
const payload = JSON.stringify({