'use client' import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts' import { Users, ShieldAlert, HeartPulse, TrendingUp, Bot, Flame, ArrowUpRight, ArrowDownRight, Scan, Zap, ExternalLink, Timer, ToggleLeft, ToggleRight } from 'lucide-react' import { StatCard } from '@/components/ui/StatCard' import { CampaignBadge } from '@/components/ui/CampaignBadge' import { AgentFeed } from '@/components/ui/AgentFeed' import { useToast } from '@/components/ui/Toast' import { cn, fmtNum, fmtUsd } from '@/lib/utils' import { stats, events, campaigns, retentionData, churnData, protocols } from '@/lib/mock-data' import type { CampaignType } from '@/lib/types' import { useState, useEffect, useCallback, useRef } from 'react' const Tip = ({ active, payload, label }: any) => { if (!active || !payload?.length) return null return (

{label}

{payload[0].value}

) } const riskDist = [ { name: 'Safe', value: 45, color: '#2dbdb6' }, { name: 'Low', value: 25, color: '#0ecb81' }, { name: 'Medium', value: 15, color: '#FCD535' }, { name: 'High', value: 10, color: '#ff9500' }, { name: 'Critical', value: 5, color: '#f6465d' }, ] interface LiveEvent { ingestionId: string; wallet: string; eventName: string risk?: string; score?: number; firedAt: string; source: string } const RISK_COLOR: Record = { critical: '#f6465d', high: '#ff9500', medium: '#FCD535', low: '#0ecb81', safe: '#2dbdb6' } const AUTO_SCAN_INTERVAL = 30 export default function DashboardPage() { const active = campaigns.filter(c => c.status === 'active') const { fire: toast } = useToast() const [scanning, setScanning] = useState(false) const [autoScan, setAutoScan] = useState(false) const [countdown, setCountdown] = useState(AUTO_SCAN_INTERVAL) const [liveEvents, setLiveEvents] = useState([]) const [sessionCount, setSessionCount] = useState(0) const [torqueStatus, setTorqueStatus] = useState<'connected' | 'unconfigured' | null>(null) const prevCountRef = useRef(0) useEffect(() => { fetch('/api/torque/status').then(r => r.json()).then(d => { setTorqueStatus(d.configured ? 'connected' : 'unconfigured') setSessionCount(d.sessionEvents || 0) }).catch(() => setTorqueStatus('unconfigured')) }, []) useEffect(() => { const poll = () => { fetch('/api/torque/events/recent?limit=8').then(r => r.json()).then(d => { const newEvents: LiveEvent[] = d.events || [] const newCount: number = d.total || 0 if (newCount > prevCountRef.current && prevCountRef.current > 0) { const latest = newEvents[0] if (latest) { toast({ type: 'event', title: latest.eventName.replace(/_/g, ' ').toUpperCase(), body: `${latest.wallet.slice(0, 8)}...${latest.wallet.slice(-4)} · ${latest.ingestionId.slice(0, 8)}`, }) } } prevCountRef.current = newCount setLiveEvents(newEvents) setSessionCount(newCount) }).catch(() => {}) } poll() const iv = setInterval(poll, 5000) return () => clearInterval(iv) }, [toast]) const triggerScan = useCallback(async () => { if (scanning) return setScanning(true) try { const res = await fetch('/api/agent/scan', { method: 'POST' }) const data = await res.json() if (data.configured) { toast({ type: 'success', title: `Scan complete — ${data.count} at-risk wallets`, body: `${data.detections?.filter((d: any) => d.eventSent).length || 0} events fired to Torque` }) } else { toast({ type: 'error', title: 'Torque not configured', body: 'Set TORQUE_INGEST_KEY in .env.local' }) } const d = await fetch('/api/torque/events/recent?limit=8').then(r => r.json()) setLiveEvents(d.events || []) setSessionCount(d.total || 0) } catch { toast({ type: 'error', title: 'Scan failed', body: 'Check server logs' }) } finally { setScanning(false) setCountdown(AUTO_SCAN_INTERVAL) } }, [scanning, toast]) // Auto-scan countdown + trigger useEffect(() => { if (!autoScan) { setCountdown(AUTO_SCAN_INTERVAL); return } const tick = setInterval(() => { setCountdown(c => { if (c <= 1) { triggerScan(); return AUTO_SCAN_INTERVAL } return c - 1 }) }, 1000) return () => clearInterval(tick) }, [autoScan, triggerScan]) // Keyboard shortcut: S = scan useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === 's' && !e.metaKey && !e.ctrlKey && e.target === document.body) triggerScan() } document.addEventListener('keydown', handler) return () => document.removeEventListener('keydown', handler) }, [triggerScan]) return (

Dashboard

Real-time churn detection and autonomous retention

{torqueStatus !== null && (
{torqueStatus === 'connected' ? 'Torque Connected' : 'Torque Unconfigured'}
)} {sessionCount > 0 && (
{sessionCount} live events
)} {/* Auto-scan toggle */}
{/* Live Torque Events Strip */} {liveEvents.length > 0 && (
Live Torque Events {sessionCount} fired this session View on Torque
{liveEvents.slice(0, 5).map((e, i) => (
{e.eventName} {e.wallet.slice(0, 8)}...{e.wallet.slice(-4)} {e.score !== undefined && score={e.score}} {e.ingestionId.slice(0, 12)}... LIVE
))}
)} {/* Auto-scan active banner */} {autoScan && (
Auto-scan active Next scan in {countdown}s — firing real Torque events for at-risk wallets
)}

Retention Rate

30-day trailing average

+9.6%
} />

Churn Rate

Daily churn percentage

-4.3%
} />

Risk Distribution

{riskDist.map((e, i) => )}
{riskDist.map(r => (
{r.name}{r.value}%
))}

Recent Events

{events.slice(0, 5).map(e => (

{e.eventType.replace(/_/g, ' ').toUpperCase()}

{e.wallet}

{e.timestamp}
))}

Active Campaigns

{active.slice(0, 4).map(c => (

{c.name}

{fmtNum(c.participantCount)} users

{c.createdBy === 'ai-agent' && }
))}

Protocol Performance

{protocols.map(p => ( ))}
Protocol Volume Users Churn Retention Avg Streak
{p.protocol}
{fmtUsd(p.volume)} {fmtNum(p.users)} {p.churnRate}% = 70 ? 'text-trading-up' : 'text-brand-yellow')}>{p.retentionRate}%
{p.avgStreak}d
) }