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
@@ -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