feat: update dependencies and improve dashboard functionality
Deploy to VPS / deploy (push) Failing after 1m43s

- 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.
This commit is contained in:
asepharyana
2026-07-26 14:27:36 +07:00
parent 9ecc4a6caa
commit 0a6a9fd982
62 changed files with 2764 additions and 305 deletions
@@ -24,10 +24,7 @@ import { registerReactionCapture } from "../modules/reaction-tracking/index.js";
import { registerThreadCapture } from "../modules/thread-tracking/index.js";
import { registerPresenceCapture } from "../modules/user-presence/index.js";
import { VoicePcmWsClient } from "../modules/voice-pcm-ws/index.js";
import {
startMuxerWorker,
stopMuxerWorker,
} from "../modules/voice-recording/muxer.js";
import { startMuxerWorker } from "../modules/voice-recording/muxer.js";
import {
setPcmWsClient,
setEventBroadcaster as setRecorderEventBroadcaster,
@@ -19,7 +19,6 @@ import {
} from "./batchProcessor.js";
import { scheduleConversationAnalysis } from "./batchScheduler.js";
import {
_redisEventBroadcaster,
broadcastAnalysisCompleted,
conversationConsecutiveErrors,
conversationDebounceTimers,
@@ -16,13 +16,13 @@ let activeCount = 0;
let pendingCount = 0;
// Track queue state changes for logging
function updateCounts(): void {
function _updateCounts(): void {
// p-limit exposes queueSize and activeCount via constructor internals,
// but we track via our wrapper to avoid depending on internals.
}
export async function withLlmConcurrency<T>(fn: () => Promise<T>): Promise<T> {
const queuedAt = activeCount + pendingCount;
const _queuedAt = activeCount + pendingCount;
pendingCount++;
logger.debug(
{ activeCount, pendingCount, maxConcurrent: config.AI_LLM_MAX_CONCURRENT },
@@ -2,10 +2,7 @@ import { createChildLogger } from "@bete/shared/logger";
import { and, desc, eq, sql } from "drizzle-orm";
import { config } from "../../shared/config/config.js";
import { getDatabase } from "../../shared/database/drizzle.js";
import {
channelCulturesTable,
messagesTable,
} from "../../shared/database/schema.js";
import { messagesTable } from "../../shared/database/schema.js";
import { updateChannelCulture } from "./channelCultureStore.js";
import { llmChat } from "./llmClient.js";
@@ -550,7 +550,7 @@ async function downloadMediaCandidate(
imageMap: Map<string, MessageImagePart[]>,
mediaAnalysisMap: Map<string, string[]>,
): Promise<void> {
const log = createChildLogger("mediaAnalysis");
const _log = createChildLogger("mediaAnalysis");
if ((imageMap.get(targetId)?.length ?? 0) >= 8) return;
if (candidate.customEmojiId || candidate.stickerName) {
@@ -646,7 +646,7 @@ export async function prepareMediaMessage(
target: MessageRecord,
allAttachments: AttachmentRecord[] | undefined,
): Promise<PreparedMediaMessage> {
const log = createChildLogger("mediaAnalysis");
const _log = createChildLogger("mediaAnalysis");
const targetId = target.id;
const imageMap = new Map<string, MessageImagePart[]>();
const webTextMap = new Map<string, string[]>();
@@ -10,7 +10,6 @@ import { delay, retryWithBackoff } from "@bete/shared/utils";
import type { ChatCompletion } from "openai/resources/chat/completions";
import { config } from "../../shared/config/config.js";
import { extractMessageMediaEvidence } from "../message-capture/messageMetadata.js";
import { getMessageById } from "../message-capture/messageStore.js";
import type {
AnalysisResult,
AttachmentRecord,
@@ -18,15 +17,7 @@ import type {
} from "../message-capture/types.js";
import { getChannelCulture } from "./channelCultureStore.js";
import { llmChat } from "./llmClient.js";
import type {
MessageImagePart,
PreparedMediaMessage,
} from "./mediaAnalysisClient.js";
import {
analyzeSingleMediaImage,
hasMediaContent,
prepareMediaMessage,
} from "./mediaAnalysisClient.js";
import { hasMediaContent, prepareMediaMessage } from "./mediaAnalysisClient.js";
import {
buildReferenceXml,
escapeXml,
@@ -816,7 +807,7 @@ export async function runSimpleTextFallback(
const MAX_CONTENT_CHARS = 500;
const truncatedContent =
content.length > MAX_CONTENT_CHARS
? content.slice(0, MAX_CONTENT_CHARS) + "..."
? `${content.slice(0, MAX_CONTENT_CHARS)}...`
: content;
let userProfileCtx = "";
@@ -876,7 +867,7 @@ Jawab HANYA dengan satu kata: clean, warn, atau flagged`;
let category = "";
if (status === "clean") {
analysis = `${message.username ?? "user"}: ${content.length > 200 ? content.slice(0, 200) + "..." : content}. Percakapan normal, tidak ada pelanggaran.`;
analysis = `${message.username ?? "user"}: ${content.length > 200 ? `${content.slice(0, 200)}...` : content}. Percakapan normal, tidak ada pelanggaran.`;
} else {
category = status === "flagged" ? "harassment" : "spam";
const categoryOptions =
@@ -958,7 +949,7 @@ Kategori: spam`;
policyVersion: "default-simple-2026-06",
evidence:
status !== "clean"
? [content.length > 120 ? content.slice(0, 120) + "..." : content]
? [content.length > 120 ? `${content.slice(0, 120)}...` : content]
: [],
};
}
@@ -795,7 +795,7 @@ export function sanitizeAiContent(
// 3. Cap length
const capped =
escaped.length > maxLen
? escaped.slice(0, maxLen) + "…[truncated]"
? `${escaped.slice(0, maxLen)}…[truncated]`
: escaped;
// 4. Wrap in CDATA unless the caller opts out (e.g. plain-text contexts)
@@ -4,8 +4,6 @@ import { extractJson } from "./jsonExtractor.js";
import { ModerationResponseSchema } from "./moderationSchemas.js";
import {
clampScore,
DEFERRAL_ANALYSIS_PATTERN,
DEFERRAL_EXCEPTION_PATTERN,
deriveRecommendedAction,
deriveSeverity,
hasDeferralAnalysis,
@@ -46,7 +44,7 @@ export function parseModerationResponse(
let parsed: any;
try {
parsed = JSON.parse(content);
} catch (e) {
} catch (_e) {
parsed = extractJson(content);
}
@@ -90,7 +90,7 @@ export function logModerationAnalysis(
},
parseErrors: string[] = [],
): void {
const response: ModerationAnalysisResponse = {
const _response: ModerationAnalysisResponse = {
messageIds,
batchSize: messageIds.length,
model,
@@ -158,7 +158,7 @@ export function logCacheEvent(
cacheKey: string,
source: "text" | "media" | "sticker",
): void {
const event: CacheHitEvent = {
const _event: CacheHitEvent = {
type,
cacheKey,
source,
@@ -260,7 +260,7 @@ export function logAnalysisSummary(
duration_ms: durationMs,
per_message_avg_ms: Math.round(durationMs / totalMessages),
summary,
success_rate: ((successCount / totalMessages) * 100).toFixed(1) + "%",
success_rate: `${((successCount / totalMessages) * 100).toFixed(1)}%`,
},
`Analysis batch complete: ${successCount}/${totalMessages} successful in ${durationMs}ms`,
);
@@ -86,7 +86,7 @@ export async function incrementTextCacheHit(text: string): Promise<void> {
`UPDATE text_analysis_cache SET hit_count = hit_count + 1 WHERE text = $1`,
[text],
);
} catch (error) {
} catch (_error) {
// Silent fail — this is just a counter, not critical
}
}
@@ -3,7 +3,7 @@ import { isIP } from "node:net";
import { createChildLogger } from "@bete/shared/logger";
import { createAbortControllerWithTimeout } from "@bete/shared/utils";
const log = createChildLogger("urlFetcher");
const _log = createChildLogger("urlFetcher");
export interface FetchedUrlContext {
url: string;
@@ -53,14 +53,14 @@ async function isSafeUrl(urlStr: string): Promise<boolean> {
return false;
}
}
} catch (err) {
} catch (_err) {
// If DNS fails, we can't fetch it anyway
return false;
}
}
return true;
} catch (err) {
} catch (_err) {
return false;
}
}
@@ -70,7 +70,7 @@ function extractOgImage(html: string): string | null {
const ogRegex =
/<meta[^>]*(?:property|name)=["'](?:og:image|twitter:image)["'][^>]*content=["']([^"']+)["']/i;
const match = html.match(ogRegex);
if (match && match[1]) {
if (match?.[1]) {
// Unescape basic HTML entities
return match[1].replace(/&amp;/g, "&").replace(/&quot;/g, '"');
}
@@ -79,7 +79,7 @@ function extractOgImage(html: string): string | null {
const ogRegexRev =
/<meta[^>]*content=["']([^"']+)["'][^>]*(?:property|name)=["'](?:og:image|twitter:image)["']/i;
const matchRev = html.match(ogRegexRev);
if (matchRev && matchRev[1]) {
if (matchRev?.[1]) {
return matchRev[1].replace(/&amp;/g, "&").replace(/&quot;/g, '"');
}
@@ -164,7 +164,7 @@ export async function fetchUrlSafely(
// If it's HTML, try to find an og:image first (for Tenor/Giphy etc)
if (contentType.startsWith("text/html")) {
const ogImage = extractOgImage(text);
if (ogImage && ogImage.startsWith("http")) {
if (ogImage?.startsWith("http")) {
// Fetch the og:image instead
return fetchUrlSafely(ogImage, depth + 1);
}
@@ -2,10 +2,7 @@ import { createChildLogger } from "@bete/shared/logger";
import { and, desc, eq, sql } from "drizzle-orm";
import { config } from "../../shared/config/config.js";
import { getDatabase } from "../../shared/database/drizzle.js";
import {
messagesTable,
userProfilesTable,
} from "../../shared/database/schema.js";
import { messagesTable } from "../../shared/database/schema.js";
import { llmChat } from "./llmClient.js";
import { updateUserProfile } from "./userProfileStore.js";
@@ -48,7 +45,7 @@ async function learnUserProfile(
for (const msg of recentMessages) {
const ch = msg.channelId ?? "unknown";
if (!channelGroups.has(ch)) channelGroups.set(ch, []);
channelGroups.get(ch)!.push(msg);
channelGroups.get(ch)?.push(msg);
}
// Build messages text with channel context
@@ -9,10 +9,7 @@ import { createChildLogger } from "@bete/shared/logger";
import type { Client } from "discord.js-selfbot-v13";
import Redis from "ioredis";
import { config } from "../../shared/config/config.js";
import type {
VoiceController,
VoiceStatus,
} from "../voice-recording/voiceController.js";
import type { VoiceController } from "../voice-recording/voiceController.js";
import { GuildHandler } from "./guild.handler.js";
import {
type CommandHandlerFn,
@@ -1,8 +1,4 @@
import {
COMMAND_VOICE_DISCONNECT_GUILD,
type CommandMessage,
type CommandReply,
} from "@bete/shared";
import type { CommandMessage, CommandReply } from "@bete/shared";
import { createChildLogger } from "@bete/shared/logger";
import type { Client } from "discord.js-selfbot-v13";
import { config } from "../../shared/config/config.js";
@@ -1,6 +1,5 @@
import http from "node:http";
import { createChildLogger } from "@bete/shared/logger";
import type { AppConfig as GatewayConfig } from "../../shared/config/config.js";
import { config } from "../../shared/config/config.js";
const logger = createChildLogger("gateway-metrics");
@@ -82,7 +81,7 @@ function formatMetrics(): string {
lines.push(`${fullName} ${metric.value}`);
}
return lines.join("\n") + "\n";
return `${lines.join("\n")}\n`;
}
export function startMetricsServer(): void {
@@ -1,4 +1,3 @@
import { decodeCursor, encodeCursor } from "@bete/shared";
import { createChildLogger, type Logger } from "@bete/shared/logger";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { getDatabase } from "../../shared/database/drizzle.js";
@@ -1,15 +1,5 @@
import { createChildLogger, type Logger } from "@bete/shared/logger";
import {
and,
asc,
desc,
eq,
inArray,
isNull,
or,
type SQL,
sql,
} from "drizzle-orm";
import { and, asc, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type * as schema from "../../shared/database/schema.js";
import { messagesTable } from "../../shared/database/schema.js";
@@ -1,4 +1,4 @@
import { decodeCursor, encodeCursor, pageResult } from "@bete/shared";
import { decodeCursor, pageResult } from "@bete/shared";
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { and, desc, eq, type SQL, sql } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
@@ -1,4 +1,4 @@
import { decodeCursor, encodeCursor, pageResult } from "@bete/shared";
import { decodeCursor, pageResult } from "@bete/shared";
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { and, desc, eq, type SQL, sql } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
@@ -1,4 +1,4 @@
import { decodeCursor, encodeCursor, pageResult } from "@bete/shared";
import { decodeCursor, pageResult } from "@bete/shared";
import { createChildLogger, type Logger } from "@bete/shared/logger";
import { and, desc, eq, type SQL, sql } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
@@ -1,14 +1,8 @@
import type fs from "node:fs";
import type {
AIRecommendedAction,
AISeverity,
AIStatus,
AnalysisQueueStatus,
AttachmentRecord,
BroadcasterClient,
MessageRecord,
ModerationBroadcaster,
RoleMetadata,
UserMetadata,
VoiceRecordingUploadData,
} from "@bete/shared";
@@ -225,7 +225,7 @@ export function resolveMediaUrl(
// -- stderr (capture for diagnostics, capped at 4KB) ----------------------------------
const MAX_STDERR = 4096;
const _MAX_STDERR = 4096;
if (proc.stderr) {
proc.stderr.on("data", (chunk: Buffer) => {
stderrBuf += chunk.toString("utf8");
@@ -1,7 +1,6 @@
import { createChildLogger } from "@bete/shared/logger";
import { eq } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { config } from "../../shared/config/config.js";
import { getDatabase } from "../../shared/database/drizzle.js";
import type * as schema from "../../shared/database/schema.js";
import { muxerJobsTable } from "../../shared/database/schema.js";
@@ -1,7 +1,7 @@
import { Transform, type TransformCallback } from "node:stream";
import { createChildLogger } from "@bete/shared/logger";
const logger = createChildLogger("packet-filter");
const _logger = createChildLogger("packet-filter");
/**
* Transform stream to filter out audio packets that are too small.
@@ -19,7 +19,7 @@ export class PacketFilter extends Transform {
_transform(
chunk: Buffer,
encoding: string,
_encoding: string,
callback: TransformCallback,
): void {
this.totalCount++;
@@ -16,7 +16,6 @@ import type { VoicePcmWsClient } from "../voice-pcm-ws/index.js";
import {
createRecordingSession,
type RecordingSession,
type SessionRecordingMetadata,
} from "./recorder/sessionRecording.js";
import { createSpeakingHandler } from "./recorder/speakingHandler.js";
@@ -141,7 +140,7 @@ export async function startRecording(
activeSessions,
recordingsDir,
pcmSender: _pcmWsClient
? (pcm, userId) => _pcmWsClient!.sendPcm(userId, pcm)
? (pcm, userId) => _pcmWsClient?.sendPcm(userId, pcm)
: undefined,
});
@@ -23,7 +23,6 @@ export class VoiceTransmitter {
private readonly TRANSMIT_CHANNEL = BACKEND_VOICE_TRANSMIT;
/** Queue for PCM chunks when backpressure is active */
private backpressureQueue: Buffer[] = [];
private draining = false;
/** Serialise start/stop to prevent races between rapid toggle commands */
private gate = Promise.resolve();
/** Set true before sending SIGTERM so exit handler knows it's intentional */
@@ -176,7 +175,7 @@ export class VoiceTransmitter {
logger.info("Voice transmitter started");
} finally {
release!();
release?.();
}
}
@@ -227,7 +226,7 @@ export class VoiceTransmitter {
discordPlayer.stop("browser-bridge");
logger.info("Voice transmitter stopped");
} finally {
release!();
release?.();
}
}
@@ -1,6 +1,6 @@
import { AppError } from "@bete/shared/errors";
import { createChildLogger } from "@bete/shared/logger";
import { getVoiceConnection, type VoiceConnection } from "@discordjs/voice";
import type { VoiceConnection } from "@discordjs/voice";
import type { Client, Guild, VoiceChannel } from "discord.js-selfbot-v13";
import { discordPlayer } from "./player.js";
import { startRecording, stopRecording } from "./recorder.js";
@@ -1,5 +1,4 @@
import { createChildLogger } from "@bete/shared/logger";
import type { AppConfig as GatewayConfig } from "../../shared/config/config.js";
import { config } from "../../shared/config/config.js";
const logger = createChildLogger("webhook-notifier");
@@ -2,7 +2,6 @@ import { pgAttachmentsTable, pgMessagesTable } from "@bete/shared";
import {
bigint as pgBigint,
boolean as pgBoolean,
foreignKey as pgForeignKey,
index as pgIndex,
integer as pgInteger,
jsonb as pgJsonb,