refactor: comprehensive codebase cleanup and architecture hardening

- Sprint 1 (Quick Wins): Remove dead analytics modules, fix 4 unresolved
  imports, replace 3 console.warn with logger, remove mock-crc import
- Sprint 2 (Architecture): Create MascotChatRepository, AnalysisRepository,
  3 Zod schemas (mascot-chat, analysis, voice), deduplicate error classes,
  move 3 SQL queries from routes to repository
- Sprint 3 (Complexity): Replace 7 any types with proper interfaces,
  extract 6 helpers from prepareMediaMessage (CC 85 -> ~15)
- Sprint 4 (Config): Remove 22 dead env vars from .env, add 30 missing
  vars to .env.example, standardize naming

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-09 10:16:04 +07:00
co-authored by Claude Opus 4.8
parent d0d9e1669e
commit 4becf0d6f1
89 changed files with 1260 additions and 5499 deletions
+2 -286
View File
@@ -138,7 +138,7 @@ export interface UIState {
selectedTextChannel?: string;
selectedAnalyticsGuild?: string;
selectedAnalyticsChannel?: string;
activeTab?: "live" | "messages" | "analytics";
activeTab?: "live" | "messages";
isListening?: boolean;
isStreaming?: boolean;
}
@@ -147,7 +147,7 @@ export interface AppConfig {
monitorGuildId: string | null;
}
export type DashboardTab = "live" | "messages" | "analytics";
export type DashboardTab = "live" | "messages";
// ─── Messages ────────────────────────────────────────────────────────────────
@@ -277,287 +277,3 @@ export function updateUIState(patch: Partial<UIState>): Promise<UIState> {
body: JSON.stringify(patch),
});
}
// ─── Analytics ───────────────────────────────────────────────────────────────
export interface HourlyBucket {
hour: string;
count: number;
clean: number;
warned: number;
flagged: number;
error: number;
}
export interface TopicTrend {
topic: string;
count: number;
score: number;
}
export interface UserStat {
user_id: string;
username: string;
avatar_url: string | null;
message_count: number;
edited_count: number;
deleted_count: number;
flagged_count: number;
last_active: number;
}
export interface ModerationBreakdown {
total: number;
clean: number;
warned: number;
flagged: number;
error: number;
pending: number;
average_score: number;
}
export interface AnalyticsOverview {
period: { start: number; end: number };
messages: ModerationBreakdown;
hourly: HourlyBucket[];
topics: TopicTrend[];
top_users: UserStat[];
active_users_count: number;
total_channels: number;
}
export interface ViolatorStat {
user_id: string;
username: string;
avatar_url: string | null;
total_messages: number;
flagged_count: number;
warned_count: number;
violation_score: number;
worst_flags: string[];
last_violation: number;
}
export interface TrendBucket {
date: string;
count: number;
clean: number;
warned: number;
flagged: number;
error: number;
}
export interface HeatmapCell {
dayOfWeek: number;
hour: number;
count: number;
clean: number;
warned: number;
flagged: number;
}
export function fetchAnalyticsOverview(params: {
guildId: string;
channelId?: string;
hours?: number;
}): Promise<AnalyticsOverview> {
const sp = new URLSearchParams({
guildId: params.guildId,
...(params.channelId && { channelId: params.channelId }),
...(params.hours && { hours: String(params.hours) }),
});
return request<AnalyticsOverview>(`/api/analytics/overview?${sp}`);
}
export function fetchHourlyStats(params: {
guildId: string;
channelId?: string;
hours?: number;
}): Promise<HourlyBucket[]> {
const sp = new URLSearchParams({
guildId: params.guildId,
...(params.channelId && { channelId: params.channelId }),
...(params.hours && { hours: String(params.hours) }),
});
return request<HourlyBucket[]>(`/api/analytics/hourly?${sp}`);
}
export function fetchTopicTrends(params: {
guildId: string;
channelId?: string;
hours?: number;
}): Promise<TopicTrend[]> {
const sp = new URLSearchParams({
guildId: params.guildId,
...(params.channelId && { channelId: params.channelId }),
...(params.hours && { hours: String(params.hours) }),
});
return request<TopicTrend[]>(`/api/analytics/topics?${sp}`);
}
export function fetchLeaderboard(params: {
guildId: string;
channelId?: string;
hours?: number;
limit?: number;
}): Promise<UserStat[]> {
const sp = new URLSearchParams({
guildId: params.guildId,
...(params.channelId && { channelId: params.channelId }),
...(params.hours && { hours: String(params.hours) }),
...(params.limit && { limit: String(params.limit) }),
});
return request<UserStat[]>(`/api/analytics/leaderboard?${sp}`);
}
export function fetchModerationStats(params: {
guildId: string;
channelId?: string;
hours?: number;
}): Promise<ModerationBreakdown> {
const sp = new URLSearchParams({
guildId: params.guildId,
...(params.channelId && { channelId: params.channelId }),
...(params.hours && { hours: String(params.hours) }),
});
return request<ModerationBreakdown>(`/api/analytics/stats?${sp}`);
}
export function fetchViolators(params: {
guildId: string;
channelId?: string;
hours?: number;
limit?: number;
}): Promise<ViolatorStat[]> {
const sp = new URLSearchParams({
guildId: params.guildId,
...(params.channelId && { channelId: params.channelId }),
...(params.hours && { hours: String(params.hours) }),
...(params.limit && { limit: String(params.limit) }),
});
return request<ViolatorStat[]>(`/api/analytics/violators?${sp}`);
}
export function fetchTrend(params: {
guildId: string;
channelId?: string;
hours?: number;
}): Promise<TrendBucket[]> {
const sp = new URLSearchParams({
guildId: params.guildId,
...(params.channelId && { channelId: params.channelId }),
...(params.hours && { hours: String(params.hours) }),
});
return request<TrendBucket[]>(`/api/analytics/trend?${sp}`);
}
export function fetchHeatmap(params: {
guildId: string;
channelId?: string;
hours?: number;
}): Promise<HeatmapCell[]> {
const sp = new URLSearchParams({
guildId: params.guildId,
...(params.channelId && { channelId: params.channelId }),
...(params.hours && { hours: String(params.hours) }),
});
return request<HeatmapCell[]>(`/api/analytics/heatmap?${sp}`);
}
// ── New analytics types & endpoints ────────────────────────────────────────
export interface ModerationActionRecord {
id: string;
message_id: string | null;
user_id: string;
guild_id: string;
action_type: string;
reason: string | null;
executed_by: string | null;
status: string;
error: string | null;
created_at: number;
executed_at: number | null;
username: string;
content: string | null;
}
export interface AISeverityBreakdown {
none: number;
low: number;
medium: number;
high: number;
critical: number;
}
export interface AIRecommendedActions {
none: number;
monitor: number;
warn: number;
review: number;
delete: number;
escalate: number;
}
export interface AIStats {
total_analyzed: number;
severity: AISeverityBreakdown;
recommended_actions: AIRecommendedActions;
analysis_errors: number;
analysis_pending: number;
avg_confidence: number;
avg_score: number;
}
export interface AttachmentStats {
total_attachments: number;
uploaded: number;
pending: number;
failed: number;
total_size_bytes: number;
unique_uploaders: number;
top_mime_type: string | null;
}
export function fetchModerationActions(params: {
guildId: string;
channelId?: string;
hours?: number;
limit?: number;
}): Promise<ModerationActionRecord[]> {
const sp = new URLSearchParams({
guildId: params.guildId,
...(params.channelId && { channelId: params.channelId }),
...(params.hours && { hours: String(params.hours) }),
...(params.limit && { limit: String(params.limit) }),
});
return request<ModerationActionRecord[]>(
`/api/analytics/moderation-actions?${sp}`,
);
}
export function fetchAIStats(params: {
guildId: string;
channelId?: string;
hours?: number;
}): Promise<AIStats> {
const sp = new URLSearchParams({
guildId: params.guildId,
...(params.channelId && { channelId: params.channelId }),
...(params.hours && { hours: String(params.hours) }),
});
return request<AIStats>(`/api/analytics/ai-stats?${sp}`);
}
export function fetchAttachmentStats(params: {
guildId: string;
channelId?: string;
hours?: number;
}): Promise<AttachmentStats> {
const sp = new URLSearchParams({
guildId: params.guildId,
...(params.channelId && { channelId: params.channelId }),
...(params.hours && { hours: String(params.hours) }),
});
return request<AttachmentStats>(`/api/analytics/attachment-stats?${sp}`);
}
@@ -1,8 +1,10 @@
// ─── Audio transmit hook — captures mic, encodes to PCM, sends via WebSocket ──
import { useCallback, useRef, useState } from "react";
import { getAPIURL } from "../api/client";
import { createChildLogger } from "../logger";
const SAMPLE_RATE = 24000;
const logger = createChildLogger("useAudioTransmit");
async function sendTransmitCommand(command: string): Promise<void> {
// Send via HTTP API
@@ -12,9 +14,12 @@ async function sendTransmitCommand(command: string): Promise<void> {
body: JSON.stringify({ command }),
});
if (!resp.ok) {
console.warn("HTTP command response:", resp.status, resp.statusText);
logger.warn("HTTP command response", {
status: resp.status,
statusText: resp.statusText,
});
const text = await resp.text().catch(() => resp.statusText);
console.warn("HTTP command failed:", text);
logger.warn("HTTP command failed", { error: text });
throw new Error(`HTTP ${resp.status}: ${text}`);
}
}
@@ -72,16 +77,18 @@ export function useAudioTransmit(socketRef: {
// Base64 encode
const bytes = new Uint8Array(pcmData.buffer);
let binary = '';
let binary = "";
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
const base64 = btoa(binary);
socketRef.current.send(JSON.stringify({
type: 'voice_transmit',
buffer: base64
}));
socketRef.current.send(
JSON.stringify({
type: "voice_transmit",
buffer: base64,
}),
);
};
}, [socketRef]);
@@ -1,4 +1,7 @@
import { useCallback, useState } from "react";
import { createChildLogger } from "../logger";
const logger = createChildLogger("useMascotChat");
export interface ChatContext {
messageCount: number;
@@ -28,7 +31,7 @@ export function useMascotChat(context?: ChatContext) {
const data = (await response.json()) as { response?: string };
return data.response || fallbackResponse(message, context);
} catch (error) {
console.warn("Mascot backend unavailable, using fallback", error);
logger.warn("Mascot backend unavailable, using fallback", { error });
return fallbackResponse(message, context);
}
},
@@ -53,7 +56,10 @@ function fallbackResponse(input: string, context?: ChatContext): string {
return `Ada ${context?.messageCount || 0} pesan di konteks dashboard saat ini 📊`;
}
if (lower.includes("berapa") && (lower.includes("orang") || lower.includes("user"))) {
if (
lower.includes("berapa") &&
(lower.includes("orang") || lower.includes("user"))
) {
return `Ada ${context?.activeParticipants || 0} user aktif yang terdeteksi 👥`;
}
@@ -34,7 +34,7 @@ function generateInsight(messages: MessageRecord[]): string {
// Hitung average panjang pesan
const avgLength = Math.round(
recentMessages.reduce((sum, m) => sum + (m.content?.length || 0), 0) /
recentMessages.length
recentMessages.length,
);
// Tentukan tipe percakapan
+49
View File
@@ -0,0 +1,49 @@
// Simple logger for frontend - structured logging wrapper
type LogLevel = "debug" | "info" | "warn" | "error";
interface LogContext {
[key: string]: unknown;
}
class Logger {
constructor(private context: string) {}
private log(level: LogLevel, message: string, context?: LogContext) {
const timestamp = new Date().toISOString();
const logData = {
level,
context: this.context,
message,
timestamp,
...context,
};
// Use appropriate console method
const consoleMethod = console[level] || console.log;
consoleMethod(
`[${level.toUpperCase()}] [${this.context}]`,
message,
context || "",
);
}
debug(message: string, context?: LogContext) {
this.log("debug", message, context);
}
info(message: string, context?: LogContext) {
this.log("info", message, context);
}
warn(message: string, context?: LogContext) {
this.log("warn", message, context);
}
error(message: string, context?: LogContext) {
this.log("error", message, context);
}
}
export function createChildLogger(context: string): Logger {
return new Logger(context);
}
@@ -1,11 +1,10 @@
import { BarChart3, MessageSquare, Radio } from "lucide-react";
import { MessageSquare, Radio } from "lucide-react";
import type { DashboardTab } from "../../entities/ui/types";
import { cn } from "../lib/utils";
const tabs: Array<{ id: DashboardTab; label: string; Icon: typeof Radio }> = [
{ id: "live", label: "Live", Icon: Radio },
{ id: "messages", label: "Messages", Icon: MessageSquare },
{ id: "analytics", label: "Analytics", Icon: BarChart3 },
];
interface MobileTabBarProps {
+4 -1
View File
@@ -1,6 +1,9 @@
// ─── Shared UI barrel export ────────────────────────────────────────────────
export { EmptyStateMascot, MascotImage } from "../../widgets/mascot/MascotImage";
export {
EmptyStateMascot,
MascotImage,
} from "../../widgets/mascot/MascotImage";
export { Badge } from "./badge";
export { Button } from "./button";
export {
+2 -4
View File
@@ -157,10 +157,8 @@ export function useDashboardSocket(handlers: WsHandlers) {
handlersRef.current.onVoiceRecordingStarted?.(d),
onVoiceRecordingStopped: (d) =>
handlersRef.current.onVoiceRecordingStopped?.(d),
onVoicePcmData: (d) =>
handlersRef.current.onVoicePcmData?.(d),
onVoiceActiveUser: (d) =>
handlersRef.current.onVoiceActiveUser?.(d),
onVoicePcmData: (d) => handlersRef.current.onVoicePcmData?.(d),
onVoiceActiveUser: (d) => handlersRef.current.onVoiceActiveUser?.(d),
};
_listeners.add(wrapper);