2026-08-07 10:44:03 +07:00
|
|
|
"use client";
|
|
|
|
|
|
2026-08-24 16:32:16 +07:00
|
|
|
import {
|
2026-08-25 22:59:19 +07:00
|
|
|
AudioWaveform,
|
|
|
|
|
ChevronRight,
|
|
|
|
|
Flame,
|
2026-08-24 16:32:16 +07:00
|
|
|
MessageSquare,
|
|
|
|
|
Mic,
|
2026-08-25 22:59:19 +07:00
|
|
|
Shield,
|
2026-08-24 16:32:16 +07:00
|
|
|
ShieldAlert,
|
2026-08-25 22:59:19 +07:00
|
|
|
Terminal,
|
2026-08-24 16:32:16 +07:00
|
|
|
Users,
|
|
|
|
|
} from "lucide-react";
|
2026-08-25 22:59:19 +07:00
|
|
|
import Link from "next/link";
|
2026-08-24 16:32:16 +07:00
|
|
|
import { useEffect } from "react";
|
2026-08-15 20:03:55 +07:00
|
|
|
import { useAmbient } from "@/components/ambient/ambient-context";
|
|
|
|
|
import { AreaActivity, RadialGauge } from "@/components/charts";
|
2026-08-24 16:32:16 +07:00
|
|
|
import { GlassPanel } from "@/components/primitives";
|
|
|
|
|
import {
|
|
|
|
|
ErrorState,
|
2026-08-25 22:59:19 +07:00
|
|
|
PageTransition,
|
2026-08-24 16:32:16 +07:00
|
|
|
SkeletonHero,
|
|
|
|
|
SkeletonMetricRow,
|
|
|
|
|
SkeletonPanel,
|
|
|
|
|
} from "@/components/shared";
|
2026-08-25 22:59:19 +07:00
|
|
|
import { useActivity, useStats, useTopReactions } from "@/hooks";
|
|
|
|
|
import { useStaggerReveal } from "@/hooks/use-gsap-animation";
|
2026-08-15 17:53:48 +07:00
|
|
|
import { formatNumber } from "@/lib/format";
|
|
|
|
|
import type { DashboardStats } from "@/lib/types";
|
2026-08-15 16:21:45 +07:00
|
|
|
|
2026-08-15 17:53:48 +07:00
|
|
|
function deriveSignal(stats?: DashboardStats) {
|
2026-08-25 22:59:19 +07:00
|
|
|
if (!stats)
|
|
|
|
|
return { tone: "signal" as const, label: "NOMINAL", state: "STABLE" };
|
2026-08-15 17:53:48 +07:00
|
|
|
const total = stats.total_flagged + stats.total_clean || 1;
|
|
|
|
|
const ratio = stats.total_flagged / total;
|
2026-08-15 20:03:55 +07:00
|
|
|
if (stats.moderation_overview.error > 0)
|
2026-08-25 22:59:19 +07:00
|
|
|
return { tone: "vermilion" as const, label: "FAULT", state: "MOD_ERR" };
|
2026-08-15 20:03:55 +07:00
|
|
|
if (ratio > 0.25)
|
2026-08-25 22:59:19 +07:00
|
|
|
return {
|
|
|
|
|
tone: "vermilion" as const,
|
|
|
|
|
label: "ELEVATED",
|
|
|
|
|
state: "HIGH_RISK",
|
|
|
|
|
};
|
|
|
|
|
if (ratio > 0.1)
|
|
|
|
|
return { tone: "amber" as const, label: "WATCH", state: "ADVISORY" };
|
|
|
|
|
return { tone: "signal" as const, label: "NOMINAL", state: "OPTIMAL" };
|
2026-08-15 17:53:48 +07:00
|
|
|
}
|
2026-08-07 10:44:03 +07:00
|
|
|
|
2026-08-15 17:53:48 +07:00
|
|
|
export function DashboardView({
|
2026-08-07 10:44:03 +07:00
|
|
|
initialStats,
|
|
|
|
|
initialActivity,
|
|
|
|
|
}: {
|
|
|
|
|
initialStats?: DashboardStats;
|
2026-08-15 17:53:48 +07:00
|
|
|
initialActivity?: Awaited<ReturnType<typeof useActivity>>["data"];
|
2026-08-07 10:44:03 +07:00
|
|
|
}) {
|
2026-08-25 13:46:15 +07:00
|
|
|
const {
|
|
|
|
|
data: stats,
|
|
|
|
|
isLoading,
|
|
|
|
|
error,
|
|
|
|
|
mutate: mutateStats,
|
|
|
|
|
} = useStats(initialStats);
|
2026-08-15 17:53:48 +07:00
|
|
|
const { data: activity } = useActivity(14, initialActivity as never);
|
2026-08-24 16:32:16 +07:00
|
|
|
const { data: reactions } = useTopReactions();
|
2026-08-15 17:53:48 +07:00
|
|
|
const ambient = useAmbient();
|
2026-08-14 10:49:44 +07:00
|
|
|
|
2026-08-25 22:59:19 +07:00
|
|
|
const hudRef = useStaggerReveal<HTMLDivElement>(".hud-tile", {
|
|
|
|
|
stagger: 0.05,
|
|
|
|
|
y: 16,
|
|
|
|
|
dependencies: [stats],
|
|
|
|
|
});
|
|
|
|
|
|
2026-08-15 17:53:48 +07:00
|
|
|
useEffect(() => {
|
|
|
|
|
const s = deriveSignal(stats);
|
2026-08-15 20:03:55 +07:00
|
|
|
ambient.set(
|
|
|
|
|
s.tone,
|
|
|
|
|
0.3 + Math.min(0.5, (stats?.today_flagged ?? 0) / 50),
|
|
|
|
|
s.label,
|
|
|
|
|
);
|
2026-08-15 17:53:48 +07:00
|
|
|
}, [stats, ambient]);
|
2026-08-15 16:21:45 +07:00
|
|
|
|
2026-08-25 13:46:15 +07:00
|
|
|
if (error && !stats)
|
|
|
|
|
return <ErrorState error={error} onRetry={() => void mutateStats()} />;
|
2026-08-24 16:32:16 +07:00
|
|
|
if (!stats && isLoading)
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-5">
|
|
|
|
|
<SkeletonHero />
|
|
|
|
|
<SkeletonMetricRow cols={4} />
|
|
|
|
|
<SkeletonPanel rows={5} />
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-08-25 13:46:15 +07:00
|
|
|
if (!stats)
|
|
|
|
|
return (
|
|
|
|
|
<ErrorState
|
2026-08-25 22:59:19 +07:00
|
|
|
error={error ?? new Error("No telemetry stream")}
|
2026-08-25 13:46:15 +07:00
|
|
|
onRetry={() => void mutateStats()}
|
|
|
|
|
/>
|
|
|
|
|
);
|
2026-08-07 10:44:03 +07:00
|
|
|
|
2026-08-15 20:03:55 +07:00
|
|
|
const s = stats;
|
2026-08-15 17:53:48 +07:00
|
|
|
const total = s.total_flagged + s.total_clean || 1;
|
|
|
|
|
const cleanRatio = s.total_clean / total;
|
2026-08-25 22:59:19 +07:00
|
|
|
const sig = deriveSignal(s);
|
2026-08-15 17:05:25 +07:00
|
|
|
|
2026-08-15 16:21:45 +07:00
|
|
|
return (
|
2026-08-25 22:59:19 +07:00
|
|
|
<PageTransition>
|
|
|
|
|
<div ref={hudRef} className="space-y-4">
|
|
|
|
|
{/* Tactical HUD Header Bar */}
|
|
|
|
|
<div className="hud-tile flex flex-wrap items-center justify-between gap-3 border-b border-hairline pb-3">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="relative flex size-3 items-center justify-center">
|
|
|
|
|
<span className="absolute inline-flex size-full animate-ping rounded-full bg-signal opacity-75" />
|
|
|
|
|
<span className="relative inline-flex size-2 rounded-full bg-signal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-baseline gap-2">
|
|
|
|
|
<h1 className="font-mono text-xs font-semibold tracking-widest text-ink uppercase">
|
|
|
|
|
GMW TELEMETRY · MATRIX_01
|
|
|
|
|
</h1>
|
|
|
|
|
<span className="font-mono text-[10px] text-ink-faint">
|
|
|
|
|
[LIVE_MONITOR]
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-08-24 16:32:16 +07:00
|
|
|
</div>
|
2026-08-25 22:59:19 +07:00
|
|
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<div className="inline-flex items-center gap-1.5 rounded-sm bg-surface px-2 py-0.5 font-mono text-[11px] text-ink-soft">
|
|
|
|
|
<span className="text-ink-faint">SYS_STATUS:</span>
|
|
|
|
|
<span className="font-bold text-signal">{sig.state}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="inline-flex items-center gap-1.5 rounded-sm bg-surface px-2 py-0.5 font-mono text-[11px] text-ink-soft">
|
|
|
|
|
<span className="text-ink-faint">VER:</span>
|
|
|
|
|
<span className="text-ink">v2.4-GSAP</span>
|
|
|
|
|
</div>
|
2026-08-24 16:32:16 +07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-08-25 22:59:19 +07:00
|
|
|
{/* Spatial HUD Grid: Live Mission Control */}
|
|
|
|
|
<div className="grid gap-4 lg:grid-cols-12">
|
|
|
|
|
{/* Main Telemetry & Activity Area */}
|
|
|
|
|
<div className="space-y-4 lg:col-span-8">
|
|
|
|
|
{/* Primary Telemetry Cluster */}
|
|
|
|
|
<div className="hud-tile grid grid-cols-2 gap-2.5 sm:grid-cols-4">
|
|
|
|
|
<div className="group relative overflow-hidden rounded-md border border-hairline bg-surface/50 p-3.5 backdrop-blur-md transition-colors hover:border-signal/40">
|
|
|
|
|
<div className="flex items-center justify-between text-ink-faint">
|
|
|
|
|
<span className="font-mono text-[10px] tracking-wider uppercase">
|
|
|
|
|
Messages
|
2026-08-24 16:32:16 +07:00
|
|
|
</span>
|
2026-08-25 22:59:19 +07:00
|
|
|
<MessageSquare className="size-3.5 text-signal" />
|
2026-08-24 16:32:16 +07:00
|
|
|
</div>
|
2026-08-25 22:59:19 +07:00
|
|
|
<div className="mt-2 font-mono text-xl font-bold tracking-tight text-ink">
|
|
|
|
|
{formatNumber(s.total_messages)}
|
2026-08-24 16:32:16 +07:00
|
|
|
</div>
|
2026-08-25 22:59:19 +07:00
|
|
|
<div className="mt-1 flex items-center gap-1 font-mono text-[10px] text-ink-faint">
|
|
|
|
|
<span className="text-signal">↑ live</span> ingest
|
2026-08-24 16:32:16 +07:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-08-25 22:59:19 +07:00
|
|
|
|
|
|
|
|
<div className="group relative overflow-hidden rounded-md border border-hairline bg-surface/50 p-3.5 backdrop-blur-md transition-colors hover:border-signal/40">
|
|
|
|
|
<div className="flex items-center justify-between text-ink-faint">
|
|
|
|
|
<span className="font-mono text-[10px] tracking-wider uppercase">
|
|
|
|
|
Flagged
|
|
|
|
|
</span>
|
|
|
|
|
<ShieldAlert className="size-3.5 text-vermilion" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-2 font-mono text-xl font-bold tracking-tight text-ink">
|
|
|
|
|
{formatNumber(s.total_flagged)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 font-mono text-[10px] text-ink-faint">
|
|
|
|
|
<span className="text-vermilion">{s.today_flagged}</span>{" "}
|
|
|
|
|
today
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="group relative overflow-hidden rounded-md border border-hairline bg-surface/50 p-3.5 backdrop-blur-md transition-colors hover:border-signal/40">
|
|
|
|
|
<div className="flex items-center justify-between text-ink-faint">
|
|
|
|
|
<span className="font-mono text-[10px] tracking-wider uppercase">
|
|
|
|
|
Active 24h
|
|
|
|
|
</span>
|
|
|
|
|
<Users className="size-3.5 text-signal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-2 font-mono text-xl font-bold tracking-tight text-ink">
|
|
|
|
|
{formatNumber(s.active_users_24h)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 font-mono text-[10px] text-ink-faint">
|
|
|
|
|
<span>unique accounts</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="group relative overflow-hidden rounded-md border border-hairline bg-surface/50 p-3.5 backdrop-blur-md transition-colors hover:border-signal/40">
|
|
|
|
|
<div className="flex items-center justify-between text-ink-faint">
|
|
|
|
|
<span className="font-mono text-[10px] tracking-wider uppercase">
|
|
|
|
|
Voice Captures
|
|
|
|
|
</span>
|
|
|
|
|
<Mic className="size-3.5 text-signal" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-2 font-mono text-xl font-bold tracking-tight text-ink">
|
|
|
|
|
{formatNumber(s.total_voice_recordings)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 font-mono text-[10px] text-ink-faint">
|
|
|
|
|
<span>audio buffers</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Activity Waveform / Time Series */}
|
|
|
|
|
<GlassPanel className="hud-tile rounded-md border border-hairline bg-surface/40 p-4 backdrop-blur-md">
|
|
|
|
|
<div className="mb-4 flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="font-mono text-[10px] tracking-widest text-ink-faint uppercase">
|
|
|
|
|
CHRONO TELEMETRY (14D)
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm font-medium text-ink">
|
|
|
|
|
Message & Activity Stream
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-3 font-mono text-[11px]">
|
|
|
|
|
<div className="flex items-center gap-1 text-ink-soft">
|
|
|
|
|
<span className="size-2 rounded-full bg-signal" />
|
|
|
|
|
<span>Activity Trend</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="h-56 w-full">
|
|
|
|
|
{activity?.daily ? (
|
|
|
|
|
<AreaActivity daily={activity.daily} />
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex h-full items-center justify-center font-mono text-xs text-ink-faint">
|
|
|
|
|
ACQUIRING SIGNAL...
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</GlassPanel>
|
|
|
|
|
|
|
|
|
|
{/* Quick Tactical Links */}
|
|
|
|
|
<div className="hud-tile grid grid-cols-1 gap-2.5 sm:grid-cols-3">
|
|
|
|
|
<Link
|
|
|
|
|
href="/voice"
|
|
|
|
|
className="group flex items-center justify-between rounded-md border border-hairline bg-surface/30 p-3 transition hover:border-signal/50 hover:bg-surface/60"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
<div className="flex size-8 items-center justify-center rounded bg-canvas-2 text-signal group-hover:bg-signal group-hover:text-signal-ink">
|
|
|
|
|
<AudioWaveform className="size-4" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="font-mono text-xs font-semibold text-ink">
|
|
|
|
|
Voice Matrix
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-[11px] text-ink-faint">
|
|
|
|
|
Realtime speaker stream
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ChevronRight className="size-4 text-ink-faint group-hover:text-signal" />
|
|
|
|
|
</Link>
|
|
|
|
|
|
|
|
|
|
<Link
|
|
|
|
|
href="/messages"
|
|
|
|
|
className="group flex items-center justify-between rounded-md border border-hairline bg-surface/30 p-3 transition hover:border-signal/50 hover:bg-surface/60"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
<div className="flex size-8 items-center justify-center rounded bg-canvas-2 text-signal group-hover:bg-signal group-hover:text-signal-ink">
|
|
|
|
|
<Terminal className="size-4" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="font-mono text-xs font-semibold text-ink">
|
|
|
|
|
Chat Log Stream
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-[11px] text-ink-faint">
|
|
|
|
|
Live telemetry feed
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ChevronRight className="size-4 text-ink-faint group-hover:text-signal" />
|
|
|
|
|
</Link>
|
|
|
|
|
|
|
|
|
|
<Link
|
|
|
|
|
href="/moderation"
|
|
|
|
|
className="group flex items-center justify-between rounded-md border border-hairline bg-surface/30 p-3 transition hover:border-signal/50 hover:bg-surface/60"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
<div className="flex size-8 items-center justify-center rounded bg-canvas-2 text-signal group-hover:bg-signal group-hover:text-signal-ink">
|
|
|
|
|
<Shield className="size-4" />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div className="font-mono text-xs font-semibold text-ink">
|
|
|
|
|
Auto-Mod Radar
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-[11px] text-ink-faint">
|
|
|
|
|
Classification & audits
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<ChevronRight className="size-4 text-ink-faint group-hover:text-signal" />
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
2026-08-24 16:32:16 +07:00
|
|
|
</div>
|
2026-08-25 22:59:19 +07:00
|
|
|
|
|
|
|
|
{/* Side Telemetry Panel: Health, Dials, & Nodes */}
|
|
|
|
|
<div className="space-y-4 lg:col-span-4">
|
|
|
|
|
{/* Safety & Moderation Integrity Gauge */}
|
|
|
|
|
<GlassPanel className="hud-tile rounded-md border border-hairline bg-surface/40 p-4 backdrop-blur-md">
|
|
|
|
|
<div className="font-mono text-[10px] tracking-widest text-ink-faint uppercase">
|
|
|
|
|
INTEGRITY MATRIX
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-1 text-sm font-medium text-ink">
|
|
|
|
|
Clean Stream Ratio
|
|
|
|
|
</div>
|
|
|
|
|
<div className="my-4 flex items-center justify-center">
|
|
|
|
|
<RadialGauge
|
|
|
|
|
value={Math.round(cleanRatio * 100)}
|
|
|
|
|
size={140}
|
|
|
|
|
tone={cleanRatio > 0.9 ? "signal" : "amber"}
|
|
|
|
|
label={`${Math.round(cleanRatio * 100)}%`}
|
|
|
|
|
sublabel="CLEAN RATIO"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2 border-t border-hairline pt-3 font-mono text-xs">
|
|
|
|
|
<div className="flex justify-between">
|
|
|
|
|
<span className="text-ink-faint">Clean Messages:</span>
|
|
|
|
|
<span className="font-medium text-ink">
|
|
|
|
|
{formatNumber(s.total_clean)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex justify-between">
|
|
|
|
|
<span className="text-ink-faint">Pending Flags:</span>
|
|
|
|
|
<span className="font-medium text-vermilion">
|
|
|
|
|
{formatNumber(s.total_flagged)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</GlassPanel>
|
|
|
|
|
|
|
|
|
|
{/* Top Reacted Messages / Emitters */}
|
|
|
|
|
<GlassPanel className="hud-tile rounded-md border border-hairline bg-surface/40 p-4 backdrop-blur-md">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<div className="font-mono text-[10px] tracking-widest text-ink-faint uppercase">
|
|
|
|
|
FREQUENT EMITTERS
|
|
|
|
|
</div>
|
|
|
|
|
<div className="text-sm font-medium text-ink">
|
|
|
|
|
Top Reacted Messages
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Flame className="size-4 text-amber" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="mt-3 space-y-2">
|
|
|
|
|
{reactions && reactions.length > 0 ? (
|
|
|
|
|
reactions.slice(0, 4).map((r) => (
|
|
|
|
|
<div
|
|
|
|
|
key={r.message_id}
|
|
|
|
|
className="flex items-center justify-between rounded bg-surface/30 px-2.5 py-1.5 font-mono text-xs"
|
|
|
|
|
>
|
|
|
|
|
<span className="max-w-[150px] truncate text-ink-soft">
|
|
|
|
|
{r.username ? `@${r.username}` : "anonymous"}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<span className="text-ink-faint">reacts:</span>
|
|
|
|
|
<span className="font-bold text-signal">
|
|
|
|
|
{r.reaction_count}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))
|
|
|
|
|
) : (
|
|
|
|
|
<div className="py-4 text-center font-mono text-xs text-ink-faint">
|
|
|
|
|
NO EMITTER TELEMETRY
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</GlassPanel>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-08-24 16:32:16 +07:00
|
|
|
</div>
|
2026-08-25 22:59:19 +07:00
|
|
|
</PageTransition>
|
2026-08-15 20:03:55 +07:00
|
|
|
);
|
2026-08-15 17:53:48 +07:00
|
|
|
}
|