From 38b74cc1db96d2870b7d4d2eb1b46d9b2c5a2b6f Mon Sep 17 00:00:00 2001 From: asepharyana Date: Tue, 1 Sep 2026 23:51:39 +0700 Subject: [PATCH] feat(fe): enrich dashboard, moderation, messages inspector, channels roster with full backend data - Dashboard: 6-tile metric deck (flagged today, active users 24h, mod queue), pipeline status bar, top channels ranking, enriched reactions with emojis, warned counter in gauge, 4 quick-nav links - Moderation: coverage tile + header rate, top flagged domains, top flagged channels, category/severity/action breakdown trends, coverage progress bar - Messages inspector: severity meter, confidence %, risk score, recommended action badge, reply-context chain, mention + role chips - Channels: roster/culture tabs, rich table (messages, clean, flagged, risk bar, last activity) wired to dashboard.channels endpoint --- .../src/app/(dashboard)/channels/view.tsx | 251 ++++++++++++++++-- .../src/app/(dashboard)/dashboard/view.tsx | 244 ++++++++++++++--- .../src/app/(dashboard)/messages/view.tsx | 172 +++++++++++- .../src/app/(dashboard)/moderation/view.tsx | 202 +++++++++++++- 4 files changed, 784 insertions(+), 85 deletions(-) diff --git a/services/frontend/src/app/(dashboard)/channels/view.tsx b/services/frontend/src/app/(dashboard)/channels/view.tsx index 580d3c07..5ee64b85 100644 --- a/services/frontend/src/app/(dashboard)/channels/view.tsx +++ b/services/frontend/src/app/(dashboard)/channels/view.tsx @@ -1,39 +1,234 @@ "use client"; +import { Hash } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useAmbient } from "@/components/ambient/ambient-context"; import { ChannelCultureGlossary } from "@/components/ChannelCultureGlossary"; -import { SkeletonPanel } from "@/components/shared"; -import { useChannelCultures } from "@/hooks"; -import type { ChannelCultureRow } from "@/lib/types"; +import { GlassPanel } from "@/components/primitives"; +import { + EmptyState, + ErrorState, + PageTransition, + SectionHeader, + SkeletonPanel, +} from "@/components/shared"; +import { useChannelCultures, useChannels } from "@/hooks"; +import { useStaggerReveal } from "@/hooks/use-gsap-animation"; +import { formatNumber } from "@/lib/format"; +import type { DashboardChannel } from "@/lib/types"; + +type ViewMode = "roster" | "culture"; export function ChannelsView({ initialCultures, }: { - initialCultures?: ChannelCultureRow[]; + initialCultures?: import("@/lib/types").ChannelCultureRow[]; }) { - const { data: cultures } = useChannelCultures(100, initialCultures); - return ( -
-
-
-
- - -
-

- CHANNEL ROSTER · SIGNAL_MAP -

-
-
- NODES: - {cultures?.length ?? 0} -
-
+ const [viewMode, setViewMode] = useState("roster"); + const { data: channels, isLoading, error } = useChannels(); + const ambient = useAmbient(); - {cultures ? ( - - ) : ( - - )} -
+ const containerRef = useStaggerReveal( + ".channel-roster-card", + { + stagger: 0.03, + y: 8, + dependencies: [channels?.length], + }, + ); + + useEffect(() => { + ambient.set("signal", 0.25, "channels"); + }, [ambient]); + + if (error && !channels) return ; + + return ( + +
+ {/* Tactical HUD Header Bar */} +
+
+
+ + +
+

+ Channel Roster · Signal Map +

+
+
+
+ + +
+
+ NODES: + + {channels?.length ?? 0} + +
+
+
+ + {viewMode === "culture" ? ( + + ) : ( + <> + {isLoading && !channels ? ( + + ) : !channels || channels.length === 0 ? ( + } + title="No channels discovered" + description="Channels appear here once the Discord gateway captures messages." + /> + ) : ( + + + {channels.length} channels + + } + /> +
+ + + + + + + + + + + + + {channels.map((ch) => ( + + ))} + +
ChannelMessagesCleanFlaggedRiskLast Activity
+
+
+ )} + + )} +
+
+ ); +} + +function ChannelRow({ channel }: { channel: DashboardChannel }) { + const total = channel.total_messages || 1; + const flaggedPct = channel.flagged_count + ? (channel.flagged_count / total) * 100 + : 0; + const riskTone = + flaggedPct > 10 ? "vermilion" : flaggedPct > 3 ? "amber" : "signal"; + + return ( + + +
+ +
+
+ {channel.channel_name ?? channel.channel_id.slice(0, 20)} +
+ {channel.guild_id && ( +
+ guild: {channel.guild_id.slice(0, 12)}… +
+ )} +
+
+ + + {formatNumber(channel.total_messages)} + + + + {formatNumber(channel.total_messages - channel.flagged_count)} + + + + 0 ? "text-vermilion" : "text-ink-muted" + }`} + > + {formatNumber(channel.flagged_count)} + + + +
+
+
+
+ + {flaggedPct.toFixed(1)}% + +
+ + + {channel.last_message_at ? ( + + {new Date(channel.last_message_at).toLocaleDateString()} + + ) : ( + — + )} + + + ); +} + +function ChannelsCultureView({ + initialCultures, +}: { + initialCultures?: import("@/lib/types").ChannelCultureRow[]; +}) { + const { data: cultures } = useChannelCultures(100, initialCultures); + + return cultures ? ( + + ) : ( + ); } diff --git a/services/frontend/src/app/(dashboard)/dashboard/view.tsx b/services/frontend/src/app/(dashboard)/dashboard/view.tsx index af4b4355..7e98c8b7 100644 --- a/services/frontend/src/app/(dashboard)/dashboard/view.tsx +++ b/services/frontend/src/app/(dashboard)/dashboard/view.tsx @@ -1,12 +1,18 @@ "use client"; import { + Activity, + AlertTriangle, AudioWaveform, ChevronRight, + Hash, MessageSquare, Mic, Shield, + ShieldAlert, + ShieldCheck, Users, + Zap, } from "lucide-react"; import Link from "next/link"; import { useEffect } from "react"; @@ -98,6 +104,8 @@ export function DashboardView({ const s = stats; const total = s.total_flagged + s.total_clean || 1; const cleanRatio = s.total_clean / total; + const mod = s.moderation_overview; + const hasModQueue = mod.pending > 0 || mod.processing > 0 || mod.error > 0; return ( @@ -113,13 +121,19 @@ export function DashboardView({
SIGNAL: - STABLE_STREAM + {deriveSignal(stats).label} + {hasModQueue && ( + + + {mod.pending + mod.processing + mod.error} IN QUEUE + + )}
- {/* Primary Metric Deck */} -
+ {/* Primary Metric Deck — 6 tiles */} +
} /> + 0 ? "vermilion" : "neutral"} + icon={} + /> + } + /> } /> } + label="Mod Queue" + value={formatNumber(mod.pending + mod.processing)} + hint={mod.error > 0 ? `${mod.error} errors` : "Processing pipeline"} + tone={ + mod.error > 0 + ? "vermilion" + : mod.pending > 0 + ? "amber" + : "neutral" + } + icon={} />
+ {/* Moderation Queue Status Bar */} + {hasModQueue && ( +
+ + Pipeline Status + + {mod.pending > 0 && ( + + + {mod.pending} PENDING + + )} + {mod.processing > 0 && ( + + + {mod.processing} PROCESSING + + )} + {mod.error > 0 && ( + + + {mod.error} ERROR + + )} +
+ )} + {/* Dynamic Activity Area & Telemetry Gauges */}
@@ -174,7 +235,7 @@ export function DashboardView({ />
-
+
CLEAN
@@ -187,12 +248,70 @@ export function DashboardView({ {formatNumber(s.total_flagged)}
+
+
WARNED
+
+ {formatNumber(s.total_warned)} +
+
- {/* Secondary Insights Deck */} + {/* Top Channels + Engagement Row */}
+ {/* Top Channels Ranking */} + {s.top_channels && s.top_channels.length > 0 && ( + + + View All + + + } + /> +
+ {s.top_channels.slice(0, 6).map((ch, i) => { + const maxCount = s.top_channels[0]?.message_count || 1; + const pct = Math.max( + 4, + Math.round((ch.message_count / maxCount) * 100), + ); + return ( +
+ + {String(i + 1).padStart(2, "0")} + + + + {ch.channel_name ?? ch.channel_id.slice(0, 14)} + +
+
+
+ + {formatNumber(ch.message_count)} + +
+ ); + })} +
+ + )} + + {/* Top Reacted Messages */} {reactions && reactions.length > 0 && ( (
- - {r.content || "[Media/Attachment]"} - - - {formatNumber(r.reaction_count)} reactions - +
+ + {r.content || "[Media/Attachment]"} + + {r.username && ( + + @{r.username} + {r.channel_name && ` in #${r.channel_name}`} + + )} +
+
+ + {formatNumber(r.reaction_count)} + + {r.top_emojis && r.top_emojis.length > 0 && ( +
+ {r.top_emojis.map((e) => ( + + {e.emoji} + + ))} +
+ )} +
))}
)} +
- -
- -
- - - - Review Live Moderation Queue - - - - - - - Inspect Active Voice Stages - - - -
-
-
+ {/* Quick Navigation */} +
+ + + + Live Moderation Queue + + + + + + + Active Voice Stages + + + + + + + Flagged Content Analysis + + + + + + + Voice Recording Archive + + +
diff --git a/services/frontend/src/app/(dashboard)/messages/view.tsx b/services/frontend/src/app/(dashboard)/messages/view.tsx index c792d761..197f553d 100644 --- a/services/frontend/src/app/(dashboard)/messages/view.tsx +++ b/services/frontend/src/app/(dashboard)/messages/view.tsx @@ -572,6 +572,34 @@ function AiBadge({ ); } +function severityTone( + sev: import("@/lib/types").AiSeverity | null | undefined, +): "signal" | "amber" | "vermilion" | "neutral" { + if (sev === "critical" || sev === "high") return "vermilion"; + if (sev === "medium") return "amber"; + if (sev === "low") return "neutral"; + return "neutral"; +} + +function severityWidth( + sev: import("@/lib/types").AiSeverity | null | undefined, +): number { + if (sev === "critical") return 100; + if (sev === "high") return 75; + if (sev === "medium") return 50; + if (sev === "low") return 25; + return 0; +} + +const RECOMMEND_LABELS: Record = { + none: "No Action", + monitor: "Monitor", + warn: "Warn User", + review: "Manual Review", + delete: "Delete Message", + escalate: "Escalate", +}; + function MessageDetail({ m, attachments, @@ -585,6 +613,14 @@ function MessageDetail({ (m as { edit_history?: Array<{ old_content: string; edited_at: number }> }) .edit_history ?? []; const editCount = (m as { edit_count?: number }).edit_count ?? 0; + const ref = parseMeta(m.metadata)?.reference; + const mentions = parseMeta(m.metadata)?.mentionedUsers ?? []; + const mentionRoles = parseMeta(m.metadata)?.mentionedRoles ?? []; + const sev = m.ai_severity; + const conf = m.ai_confidence; + const recAction = m.ai_recommended_action; + const score = m.ai_moderation_score; + return (
@@ -615,11 +651,89 @@ function MessageDetail({
+ {/* Reply context chain */} + {ref && (ref.content || ref.repliedUsername) && ( +
+ + Replying to {ref.repliedUsername ?? "message"} + + {ref.content && ( +
+ {ref.content} +
+ )} +
+ )} +
{renderMessageContent(m.edited_content ?? m.content, m.metadata) || "(no text content)"}
+ {/* AI Analysis Metrics — severity, confidence, score, recommended action */} + {(sev || conf != null || score != null || recAction) && ( +
+ {sev && sev !== "none" && ( +
+
Severity
+
+
+
+
+ + {sev.toUpperCase()} + +
+
+ )} + {conf != null && ( +
+
Confidence
+
+ {Math.round(conf * 100)}% +
+
+ )} + {score != null && ( +
+
Risk Score
+
+ {score.toFixed(2)} +
+
+ )} + {recAction && recAction !== "none" && ( +
+
Recommended Action
+ + {RECOMMEND_LABELS[recAction] ?? recAction} + +
+ )} +
+ )} + {m.ai_analysis && (
AI heuristic reasoning
@@ -629,18 +743,52 @@ function MessageDetail({
)} - {(flags.length > 0 || cats.length > 0) && ( -
- {flags.map((f) => ( - - {f} - - ))} - {cats.map((c) => ( - - {c} - - ))} + {/* Flags / Categories / Mentions */} + {(flags.length > 0 || + cats.length > 0 || + mentions.length > 0 || + mentionRoles.length > 0) && ( +
+ {(flags.length > 0 || cats.length > 0) && ( +
+ {flags.map((f) => ( + + {f} + + ))} + {cats.map((c) => ( + + {c} + + ))} +
+ )} + {mentions.length > 0 && ( +
+ Mentions: + {mentions.map((u) => ( + + @{u.username} + + ))} +
+ )} + {mentionRoles.length > 0 && ( +
+ Roles: + {mentionRoles.map((r) => ( + + @{r.name} + + ))} +
+ )}
)} diff --git a/services/frontend/src/app/(dashboard)/moderation/view.tsx b/services/frontend/src/app/(dashboard)/moderation/view.tsx index 3ab3e2c6..c17c3e0f 100644 --- a/services/frontend/src/app/(dashboard)/moderation/view.tsx +++ b/services/frontend/src/app/(dashboard)/moderation/view.tsx @@ -1,11 +1,20 @@ "use client"; -import { AlertTriangle, CheckCircle2, Shield } from "lucide-react"; +import { + AlertTriangle, + BarChart3, + CheckCircle2, + Globe, + Hash, + Shield, + TrendingUp, +} from "lucide-react"; import { useEffect, useState } from "react"; import { useAmbient } from "@/components/ambient/ambient-context"; import { LiveModerationFeed } from "@/components/LiveModerationFeed"; import { ModerationHeatmap } from "@/components/ModerationHeatmap"; import { GlassPanel } from "@/components/primitives"; +import { ScamDomains } from "@/components/ScamDomains"; import { ErrorState, MetricTile, @@ -14,10 +23,15 @@ import { SkeletonMetricRow, SkeletonPanel, } from "@/components/shared"; +import { TopChannels } from "@/components/TopChannels"; import { useHourlyModeration, useModerationActions, + useModerationCoverage, useModerationStats, + useModerationTrends, + useTopFlaggedChannels, + useTopFlaggedDomains, } from "@/hooks"; import { useStaggerReveal } from "@/hooks/use-gsap-animation"; import { formatNumber } from "@/lib/format"; @@ -48,6 +62,10 @@ export function ModerationView({ initialActions, ); const { data: hourly } = useHourlyModeration(); + const { data: domains } = useTopFlaggedDomains(30); + const { data: channels } = useTopFlaggedChannels(30); + const { data: trends } = useModerationTrends(30); + const { data: coverage } = useModerationCoverage(30); const ambient = useAmbient(); const [filterMode, setFilterMode] = useState<"all" | "flagged" | "clean">( @@ -99,6 +117,26 @@ export function ModerationView({ Moderation Intelligence Console
+
+ EXECUTION: + + {Math.round((executed / total) * 100)}% RATE + + {coverage && ( + <> + COVERAGE: + = 90 + ? "bg-signal/15 text-signal border-signal/30" + : "bg-amber/15 text-amber border-amber/30" + }`} + > + {Math.round(coverage.coverage_rate)}% + + + )} +
{(["all", "flagged", "clean"] as const).map((mode) => (