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:
co-authored by
Claude Opus 4.8
parent
d0d9e1669e
commit
4becf0d6f1
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user