From c9e79c8c7cb3de50beda0d8717853a89810008b1 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Sat, 30 May 2026 20:40:33 +0700 Subject: [PATCH] feat: refactor database access in analyticsStore to use executeAll and executeGet for improved query handling --- .../0004_analytics_perf_indexes.sql | 16 + .../components/analytics/AnalyticsPanel.tsx | 50 +-- frontend/src/hooks/useAnalytics.ts | 112 +++-- frontend/src/main.tsx | 16 +- package.json | 1 + pnpm-lock.yaml | 18 + src/database/drizzle.ts | 39 ++ src/moderation/analyticsStore.ts | 416 ++++++------------ 8 files changed, 326 insertions(+), 342 deletions(-) create mode 100644 drizzle/migrations/0004_analytics_perf_indexes.sql diff --git a/drizzle/migrations/0004_analytics_perf_indexes.sql b/drizzle/migrations/0004_analytics_perf_indexes.sql new file mode 100644 index 0000000..7c94b5c --- /dev/null +++ b/drizzle/migrations/0004_analytics_perf_indexes.sql @@ -0,0 +1,16 @@ +-- Add composite index for analytics queries: every analytics query filters by +-- guild_id + created_at range + deleted_at IS NULL. +-- This single index covers getHourlyStats, getTopicTrends, getUserLeaderboard, +-- getModerationStats, getActiveChannelCount, and getTopViolators. +CREATE INDEX IF NOT EXISTS "idx_messages_guild_created" + ON "messages" USING btree ("guild_id", "created_at"); + +-- Covering index for analytics queries that also filter by ai_status +-- (flagged/warn/clean counts). This speeds up the GROUP BY ai_status aggregates. +CREATE INDEX IF NOT EXISTS "idx_messages_guild_status_created" + ON "messages" USING btree ("guild_id", "ai_status", "created_at"); + +-- Composite index for channel-scoped analytics queries +-- Covers channel_id + thread_id OR filters used when a specific channel is selected +CREATE INDEX IF NOT EXISTS "idx_messages_guild_channel_created" + ON "messages" USING btree ("guild_id", "channel_id", "created_at"); diff --git a/frontend/src/components/analytics/AnalyticsPanel.tsx b/frontend/src/components/analytics/AnalyticsPanel.tsx index b945d96..87b7de0 100644 --- a/frontend/src/components/analytics/AnalyticsPanel.tsx +++ b/frontend/src/components/analytics/AnalyticsPanel.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { motion, AnimatePresence } from "motion/react"; import { Activity, @@ -17,8 +17,7 @@ import { } from "lucide-react"; import type { Channel, Guild } from "../../types/voice"; import { useAnalytics } from "../../hooks/useAnalytics"; -import type { AnalyticsOverview, HourlyBucket, TopicTrend, UserStat, ViolatorStat } from "../../api/analytics"; -import { fetchViolators } from "../../api/analytics"; +import type { AnalyticsOverview, HourlyBucket, TopicTrend, UserStat, ViolatorStat } from "../../hooks/useAnalytics"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; import { Select } from "../ui/select"; import { Button } from "../ui/button"; @@ -63,36 +62,25 @@ export function AnalyticsPanel({ onChannelChange, }: AnalyticsPanelProps) { const [hours, setHours] = useState(24); - const [violators, setViolators] = useState([]); - const [violatorsLoading, setViolatorsLoading] = useState(false); - const { overview, loading, error, refresh } = useAnalytics({ + const { + overview, + isLoading, + isFetching, + error, + refresh, + violators, + violatorsLoading, + violatorsFetching, + refreshViolators, + } = useAnalytics({ guildId: selectedGuild, channelId: selectedChannel || undefined, hours, }); - const loadViolators = useCallback(async () => { - if (!selectedGuild) return; - setViolatorsLoading(true); - try { - const data = await fetchViolators({ - guildId: selectedGuild, - channelId: selectedChannel || undefined, - hours, - limit: 20, - }); - setViolators(data); - } catch { - // silent - } finally { - setViolatorsLoading(false); - } - }, [selectedGuild, selectedChannel, hours]); - - useEffect(() => { - loadViolators(); - }, [loadViolators]); + // Loading is true only on first load (no cached data); fetching means background refresh + const loading = isLoading && !isFetching; return (
@@ -155,11 +143,11 @@ export function AnalyticsPanel({ ))}