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
This commit is contained in:
asepharyana committed 2026-09-01 23:51:39 +07:00
1 parent 7bfdf9c85c
commit 38b74cc1db
4 files changed
+784 -85

No files matched your search

@@ -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 (
<PageTransition>
@@ -113,13 +121,19 @@ export function DashboardView({
<div className="flex items-center gap-2 font-mono text-[11px] text-ink-muted">
<span>SIGNAL:</span>
<span className="rounded bg-signal/15 px-2 py-0.5 font-medium text-signal border border-signal/30">
STABLE_STREAM
{deriveSignal(stats).label}
</span>
{hasModQueue && (
<span className="flex items-center gap-1 rounded bg-amber/15 px-2 py-0.5 font-medium text-amber border border-amber/30">
<Zap className="size-3" />
{mod.pending + mod.processing + mod.error} IN QUEUE
</span>
)}
</div>
</div>
{/* Primary Metric Deck */}
<div className="linear-tile grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{/* Primary Metric Deck — 6 tiles */}
<div className="linear-tile grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
<MetricTile
label="Total Messages"
value={formatNumber(s.total_messages)}
@@ -133,6 +147,19 @@ export function DashboardView({
tone="signal"
icon={<Shield className="size-4" />}
/>
<MetricTile
label="Flagged Today"
value={formatNumber(s.today_flagged)}
hint={`${formatNumber(s.total_flagged)} all-time flagged`}
tone={s.today_flagged > 0 ? "vermilion" : "neutral"}
icon={<ShieldAlert className="size-4" />}
/>
<MetricTile
label="Active Users 24h"
value={formatNumber(s.active_users_24h)}
hint={`${formatNumber(s.total_users)} total profiles`}
icon={<Users className="size-4" />}
/>
<MetricTile
label="Voice Recordings"
value={formatNumber(s.total_voice_recordings)}
@@ -140,13 +167,47 @@ export function DashboardView({
icon={<Mic className="size-4" />}
/>
<MetricTile
label="Total Profiles"
value={formatNumber(s.total_profiles)}
hint={`${formatNumber(s.active_users_24h)} active 24h`}
icon={<Users className="size-4" />}
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={<Activity className="size-4" />}
/>
</div>
{/* Moderation Queue Status Bar */}
{hasModQueue && (
<div className="linear-tile flex flex-wrap items-center gap-4 rounded-[8px] border border-amber/20 bg-amber/5 px-4 py-2.5 font-mono text-[10px] text-ink-soft">
<span className="text-amber font-semibold uppercase">
Pipeline Status
</span>
{mod.pending > 0 && (
<span className="flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-amber" />
{mod.pending} PENDING
</span>
)}
{mod.processing > 0 && (
<span className="flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-signal animate-pulse" />
{mod.processing} PROCESSING
</span>
)}
{mod.error > 0 && (
<span className="flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-vermilion" />
{mod.error} ERROR
</span>
)}
</div>
)}
{/* Dynamic Activity Area & Telemetry Gauges */}
<div className="grid gap-3 lg:grid-cols-3">
<GlassPanel className="linear-tile lg:col-span-2">
@@ -174,7 +235,7 @@ export function DashboardView({
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2 border-t border-hairline pt-3 text-center">
<div className="grid grid-cols-3 gap-2 border-t border-hairline pt-3 text-center">
<div>
<div className="eyebrow">CLEAN</div>
<div className="font-sans text-sm font-semibold text-success">
@@ -187,12 +248,70 @@ export function DashboardView({
{formatNumber(s.total_flagged)}
</div>
</div>
<div>
<div className="eyebrow">WARNED</div>
<div className="font-sans text-sm font-semibold text-amber">
{formatNumber(s.total_warned)}
</div>
</div>
</div>
</GlassPanel>
</div>
{/* Secondary Insights Deck */}
{/* Top Channels + Engagement Row */}
<div className="grid gap-3 lg:grid-cols-2">
{/* Top Channels Ranking */}
{s.top_channels && s.top_channels.length > 0 && (
<GlassPanel className="linear-tile">
<SectionHeader
eyebrow="volume"
title="Top Active Channels"
action={
<Link
href="/analysis"
className="flex items-center gap-1 text-xs text-ink-soft hover:text-ink"
>
View All
<ChevronRight className="size-3" />
</Link>
}
/>
<div className="mt-3 space-y-2">
{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 (
<div
key={ch.channel_id}
className="flex items-center gap-3 text-xs"
>
<span className="font-mono w-4 text-[10px] text-ink-faint">
{String(i + 1).padStart(2, "0")}
</span>
<span className="w-28 shrink-0 truncate font-mono text-xs text-ink sm:w-36">
<Hash className="mr-0.5 inline size-3 text-signal" />
{ch.channel_name ?? ch.channel_id.slice(0, 14)}
</span>
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-surface-2">
<div
className="h-full rounded-full bg-signal"
style={{ width: `${pct}%` }}
/>
</div>
<span className="font-mono w-16 text-right text-[11px] font-semibold text-signal">
{formatNumber(ch.message_count)}
</span>
</div>
);
})}
</div>
</GlassPanel>
)}
{/* Top Reacted Messages */}
{reactions && reactions.length > 0 && (
<GlassPanel className="linear-tile">
<SectionHeader
@@ -203,47 +322,86 @@ export function DashboardView({
{reactions.slice(0, 4).map((r) => (
<div
key={r.message_id}
className="hud-card flex items-center justify-between p-2.5"
className="hud-card flex items-start justify-between gap-2 p-2.5"
>
<span className="truncate pr-2 text-xs text-ink-soft">
{r.content || "[Media/Attachment]"}
</span>
<span className="font-mono text-xs font-semibold text-signal shrink-0">
{formatNumber(r.reaction_count)} reactions
</span>
<div className="min-w-0 flex-1">
<span className="line-clamp-1 block text-xs text-ink-soft">
{r.content || "[Media/Attachment]"}
</span>
{r.username && (
<span className="mt-0.5 block font-mono text-[10px] text-ink-muted">
@{r.username}
{r.channel_name && ` in #${r.channel_name}`}
</span>
)}
</div>
<div className="shrink-0 text-right">
<span className="font-mono text-xs font-semibold text-signal">
{formatNumber(r.reaction_count)}
</span>
{r.top_emojis && r.top_emojis.length > 0 && (
<div className="mt-0.5 flex gap-0.5">
{r.top_emojis.map((e) => (
<span
key={e.emoji}
className="text-[10px]"
title={`${e.emoji} ×${e.count}`}
>
{e.emoji}
</span>
))}
</div>
)}
</div>
</div>
))}
</div>
</GlassPanel>
)}
</div>
<GlassPanel className="linear-tile flex flex-col justify-between">
<div>
<SectionHeader eyebrow="Navigation" title="Quick Diagnostics" />
<div className="mt-2 space-y-1.5">
<Link
href="/moderation"
className="hud-card flex items-center justify-between p-2.5 text-xs text-ink-soft transition-colors hover:text-ink"
>
<span className="flex items-center gap-2">
<Shield className="size-3.5 text-signal" />
Review Live Moderation Queue
</span>
<ChevronRight className="size-3.5 text-ink-muted" />
</Link>
<Link
href="/voice"
className="hud-card flex items-center justify-between p-2.5 text-xs text-ink-soft transition-colors hover:text-ink"
>
<span className="flex items-center gap-2">
<AudioWaveform className="size-3.5 text-success" />
Inspect Active Voice Stages
</span>
<ChevronRight className="size-3.5 text-ink-muted" />
</Link>
</div>
</div>
</GlassPanel>
{/* Quick Navigation */}
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
<Link
href="/moderation"
className="linear-tile hud-card flex items-center justify-between p-3 text-xs text-ink-soft transition-colors hover:text-ink"
>
<span className="flex items-center gap-2">
<ShieldCheck className="size-3.5 text-signal" />
Live Moderation Queue
</span>
<ChevronRight className="size-3.5 text-ink-muted" />
</Link>
<Link
href="/voice"
className="linear-tile hud-card flex items-center justify-between p-3 text-xs text-ink-soft transition-colors hover:text-ink"
>
<span className="flex items-center gap-2">
<AudioWaveform className="size-3.5 text-success" />
Active Voice Stages
</span>
<ChevronRight className="size-3.5 text-ink-muted" />
</Link>
<Link
href="/analysis"
className="linear-tile hud-card flex items-center justify-between p-3 text-xs text-ink-soft transition-colors hover:text-ink"
>
<span className="flex items-center gap-2">
<AlertTriangle className="size-3.5 text-amber" />
Flagged Content Analysis
</span>
<ChevronRight className="size-3.5 text-ink-muted" />
</Link>
<Link
href="/recordings"
className="linear-tile hud-card flex items-center justify-between p-3 text-xs text-ink-soft transition-colors hover:text-ink"
>
<span className="flex items-center gap-2">
<Mic className="size-3.5 text-vermilion" />
Voice Recording Archive
</span>
<ChevronRight className="size-3.5 text-ink-muted" />
</Link>
</div>
</div>
</PageTransition>