From 826dfcd56cbb80c75a2032f34af2ed9cb9b42714 Mon Sep 17 00:00:00 2001 From: Developer Date: Tue, 28 Jul 2026 10:09:24 +0700 Subject: [PATCH] feat: rewrite dashboard as Ops Center with stats, live, and activity tabs Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/app/(dashboard)/dashboard/page.tsx | 123 ++++++++---------- .../src/components/dashboard/live-stream.tsx | 85 ++++++++++++ .../src/components/dashboard/mod-queue.tsx | 70 ++++++++++ 3 files changed, 210 insertions(+), 68 deletions(-) create mode 100644 services/frontend/src/components/dashboard/live-stream.tsx create mode 100644 services/frontend/src/components/dashboard/mod-queue.tsx diff --git a/services/frontend/src/app/(dashboard)/dashboard/page.tsx b/services/frontend/src/app/(dashboard)/dashboard/page.tsx index c3d7c4f..8401822 100644 --- a/services/frontend/src/app/(dashboard)/dashboard/page.tsx +++ b/services/frontend/src/app/(dashboard)/dashboard/page.tsx @@ -1,83 +1,70 @@ "use client"; -import { BarChart3, Hash, Users } from "lucide-react"; +import { AlertCircle, Clock, Hash, Shield, Sparkles, Users } from "lucide-react"; import { useState } from "react"; -import { - ChannelDetailSection, - ChannelsSection, - StatsSection, - UserDetailSection, - UsersSection, -} from "@/components/dashboard"; -import { GuildSelector } from "@/components/shared/guild-selector"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { useStats } from "@/hooks"; +import { StatCard } from "@/components/dashboard/stat-card"; +import { LiveStream } from "@/components/dashboard/live-stream"; +import { ModQueue } from "@/components/dashboard/mod-queue"; +import { MessageTrendChart } from "@/components/dashboard/message-trend-chart"; +import { ActivityHeatmap } from "@/components/dashboard/activity-heatmap"; +import { TopChannelsChart } from "@/components/dashboard/top-channels-chart"; +import { SubNav } from "@/components/layout/sub-nav"; +import { ErrorState, LoadingSkeleton } from "@/components/shared"; -type View = "stats" | "users" | "channels" | "user-detail" | "channel-detail"; +type DashboardTab = "stats" | "live" | "activity"; export default function DashboardPage() { - const [view, setView] = useState("stats"); - const [guildId, setGuildId] = useState(""); - const [selectedUserId, setSelectedUserId] = useState(null); - const [selectedChannelId, setSelectedChannelId] = useState( - null, - ); + const [tab, setTab] = useState("stats"); + const { data: stats, isLoading, error, refetch } = useStats(); + + const subNavTabs = [ + { id: "stats", label: "Stats", icon: }, + { id: "live", label: "Live", icon: }, + { id: "activity", label: "Activity", icon: }, + ]; return ( -
- +
+ setTab(t as DashboardTab)} /> - setView(v as View)} - > - - setView("stats")}> - Stats - - setView("users")}> - Users - - setView("channels")}> - Channels - - - + {tab === "stats" && ( +
+ {error ? ( + + ) : isLoading || !stats ? ( + + ) : ( + <> +
+ + + + + + +
- {view === "stats" && } - {view === "users" && ( - { - setSelectedUserId(userId); - setView("user-detail"); - }} - /> +
+ + +
+ + )} +
)} - {view === "user-detail" && selectedUserId && ( - setView("users")} - /> + + {tab === "live" && ( +
+ + +
)} - {view === "channels" && ( - { - setSelectedChannelId(chId); - setView("channel-detail"); - }} - /> - )} - {view === "channel-detail" && selectedChannelId && ( - setView("channels")} - /> + + {tab === "activity" && ( +
+ +
)}
); diff --git a/services/frontend/src/components/dashboard/live-stream.tsx b/services/frontend/src/components/dashboard/live-stream.tsx new file mode 100644 index 0000000..3502cd8 --- /dev/null +++ b/services/frontend/src/components/dashboard/live-stream.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { GlassCard } from "@/components/glass/card"; +import { useWebSocket } from "@/lib/ws/context"; +import { cn } from "@/lib/utils"; + +interface LiveMessage { + id: string; + content: string; + username: string; + channelName?: string; + timestamp: string; + flagged?: boolean; +} + +export function LiveStream() { + const [messages, setMessages] = useState([]); + const scrollRef = useRef(null); + const ws = useWebSocket(); + + useEffect(() => { + const unsub = ws.on("message_created", (data: any) => { + const msg: LiveMessage = { + id: data.id, + content: data.content || "(attachment)", + username: data.username || "unknown", + channelName: data.channelName, + timestamp: new Date().toLocaleTimeString(), + flagged: data.ai_status === "flagged" || data.ai_status === "warn", + }; + setMessages((prev) => [msg, ...prev].slice(0, 50)); + }); + return () => unsub(); + }, [ws]); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = 0; + } + }, [messages]); + + return ( + +
+ + + + + + Live Stream + +
+
+ {messages.length === 0 ? ( +
+ Waiting for messages... +
+ ) : ( + messages.map((msg) => ( +
+ + {msg.username} + + + {msg.content} + + + {msg.timestamp} + +
+ )) + )} +
+
+ ); +} diff --git a/services/frontend/src/components/dashboard/mod-queue.tsx b/services/frontend/src/components/dashboard/mod-queue.tsx new file mode 100644 index 0000000..ddd031e --- /dev/null +++ b/services/frontend/src/components/dashboard/mod-queue.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { AlertCircle, Check, Trash2 } from "lucide-react"; +import { GlassCard } from "@/components/glass/card"; +import { cn } from "@/lib/utils"; + +interface ModQueueItem { + id: string; + content: string; + username: string; + severity: "low" | "medium" | "high" | "critical"; + reason: string; +} + +export function ModQueue({ items = [] }: { items?: ModQueueItem[] }) { + const severityColor = { + low: "text-accent-amber border-accent-amber/30", + medium: "text-accent-purple border-accent-purple/30", + high: "text-destructive border-destructive/40", + critical: "text-destructive border-destructive/60 bg-destructive/10", + }; + + return ( + +
+ + + Mod Queue + + {items.length > 0 && ( + + {items.length} pending + + )} +
+
+ {items.length === 0 ? ( +
+ No flagged messages +
+ ) : ( + items.map((item) => ( +
+
+ {item.username} + {item.severity} +
+

{item.content}

+

{item.reason}

+
+ + +
+
+ )) + )} +
+
+ ); +}