muthuk1's picture
feat: recovery attribution, pre-churn warnings, recovery card, threshold editor, telegram alerts
f667d47
'use client'
import { TrendingUp, TrendingDown, Target, Zap, Award, Clock, CheckCircle2, XCircle, BarChart2 } from 'lucide-react'
import { cn, fmtNum } from '@/lib/utils'
import { cohorts, eventBreakdown, dailyEvents, roiData } from '@/lib/mock-data'
import { BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
import { useState, useEffect } from 'react'
const Tip = ({ active, payload, label }: any) => {
if (!active || !payload?.length) return null
return (<div className="bg-surface-elevated border border-hairline-dark rounded-lg p-3 shadow-lg">
<p className="text-caption text-muted mb-1">{label}</p>
{payload.map((p: any, i: number) => <p key={i} className="font-mono text-num-sm" style={{color:p.color}}>{p.name}: {typeof p.value==='number' && p.value>1000 ? fmtNum(p.value) : p.value}{p.unit||''}</p>)}
</div>)
}
function getColor(v: number) {
if (v === 0) return 'bg-surface-elevated text-muted'
if (v >= 75) return 'bg-trading-up/20 text-trading-up'
if (v >= 50) return 'bg-brand-yellow/15 text-brand-yellow'
if (v >= 30) return 'bg-[#ff9500]/15 text-[#ff9500]'
return 'bg-trading-down/15 text-trading-down'
}
interface AttributionStats {
total: number; recovered: number; pending: number; successRate: number
avgDaysToRecover: number
weeklyTrend: { week: string; rate: number }[]
interventions: { wallet: string; eventFired: string; score: number; recovered: boolean; daysToRecover?: number }[]
}
export default function AnalyticsPage() {
const maxEvt = Math.max(...eventBreakdown.map(e => e.count))
const [attr, setAttr] = useState<AttributionStats | null>(null)
useEffect(() => {
fetch('/api/torque/attribution')
.then(r => r.ok ? r.json() : null)
.then(d => d && setAttr(d))
.catch(() => {})
}, [])
return (
<div className="p-6 space-y-6">
<div><h1 className="text-display-sm text-[#eaecef]">Analytics</h1><p className="text-body-md text-muted mt-1">Deep retention insights, cohort analysis & ROI tracking</p></div>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
{[
{ l:'Avg Retention', v:'67.3%', c:'+9.6%', up:true, i:Target },
{ l:'Churn Rate', v:'4.2%', c:'-4.3%', up:false, i:TrendingDown },
{ l:'Campaign ROI', v:'847%', c:'+15.2%', up:true, i:TrendingUp },
{ l:'Saved Wallets', v:'15.2K', c:'+23.4%', up:true, i:Award },
{ l:'Agent Actions', v:'3,847', c:'+12.1%', up:true, i:Zap },
].map(k => { const I = k.i; return (
<div key={k.l} className="rounded-xl bg-surface-card border border-hairline-dark p-4">
<div className="flex items-center justify-between mb-2"><span className="text-caption text-muted">{k.l}</span><I className="w-4 h-4 text-muted"/></div>
<div className="font-mono text-title-lg text-[#eaecef] tabular-nums">{k.v}</div>
<span className={cn('font-mono text-num-sm tabular-nums', k.up?'text-trading-up':'text-trading-down')}>{k.c}</span>
</div>
)})}
</div>
{/* Recovery Attribution Dashboard */}
{attr && (
<div className="rounded-xl bg-surface-card border border-hairline-dark overflow-hidden">
<div className="px-5 py-4 border-b border-hairline-dark flex items-center justify-between">
<div className="flex items-center gap-2">
<BarChart2 className="w-4 h-4 text-trading-up" />
<h3 className="text-title-sm">Recovery Attribution</h3>
<span className="text-[10px] px-2 py-0.5 rounded-pill bg-trading-up/10 text-trading-up font-semibold border border-trading-up/20">LIVE</span>
</div>
<p className="text-caption text-muted">Did interventions actually work?</p>
</div>
<div className="p-5 space-y-5">
<div className="grid grid-cols-4 gap-4">
<div className="rounded-xl bg-trading-up/5 border border-trading-up/20 p-4 text-center">
<p className="text-caption text-trading-up/70 mb-1">Success Rate</p>
<p className="font-mono text-display-sm text-trading-up tabular-nums">{attr.successRate}%</p>
<p className="text-caption text-muted mt-0.5">{attr.recovered}/{attr.total} rescued</p>
</div>
<div className="rounded-xl bg-surface-elevated border border-hairline-dark p-4 text-center">
<p className="text-caption text-muted mb-1">Avg Recovery</p>
<p className="font-mono text-title-lg text-[#eaecef] tabular-nums">{attr.avgDaysToRecover}d</p>
<p className="text-caption text-muted mt-0.5">days to return</p>
</div>
<div className="rounded-xl bg-brand-yellow/5 border border-brand-yellow/20 p-4 text-center">
<p className="text-caption text-brand-yellow/70 mb-1">Pending</p>
<p className="font-mono text-title-lg text-brand-yellow tabular-nums">{attr.pending}</p>
<p className="text-caption text-muted mt-0.5">awaiting return</p>
</div>
<div className="rounded-xl bg-surface-elevated border border-hairline-dark p-4">
<p className="text-caption text-muted mb-2">Weekly trend</p>
<ResponsiveContainer width="100%" height={52}>
<LineChart data={attr.weeklyTrend}>
<Line type="monotone" dataKey="rate" stroke="#0ecb81" strokeWidth={2} dot={false} />
</LineChart>
</ResponsiveContainer>
</div>
</div>
<div>
<p className="text-caption text-muted uppercase tracking-wider mb-3">Recent interventions</p>
<div className="space-y-1.5">
{attr.interventions.map((item, i) => (
<div key={i} className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-surface-elevated/50 transition">
{item.recovered
? <CheckCircle2 className="w-4 h-4 text-trading-up flex-shrink-0" />
: <XCircle className="w-4 h-4 text-muted flex-shrink-0" />}
<span className="font-mono text-body-sm text-[#eaecef] w-32">{item.wallet}</span>
<code className="text-[10px] text-muted bg-surface-elevated px-1.5 py-0.5 rounded">{item.eventFired}</code>
<span className="font-mono text-num-sm text-muted tabular-nums">score={item.score}</span>
<span className={cn('ml-auto text-caption font-semibold', item.recovered ? 'text-trading-up' : 'text-muted')}>
{item.recovered ? `✓ returned in ${item.daysToRecover}d` : 'pending'}
</span>
</div>
))}
</div>
</div>
</div>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
<h3 className="text-title-sm mb-4">Daily Custom Events</h3>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={dailyEvents}><CartesianGrid strokeDasharray="3 3" stroke="#2b3139"/><XAxis dataKey="date" tick={{fill:'#707a8a',fontSize:11}} axisLine={{stroke:'#2b3139'}}/><YAxis tick={{fill:'#707a8a',fontSize:11}} axisLine={{stroke:'#2b3139'}}/><Tooltip content={<Tip/>}/><Bar dataKey="value" fill="#FCD535" radius={[4,4,0,0]} name="Events"/></BarChart>
</ResponsiveContainer>
</div>
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
<h3 className="text-title-sm mb-4">Campaign ROI Over Time</h3>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={roiData}><CartesianGrid strokeDasharray="3 3" stroke="#2b3139"/><XAxis dataKey="date" tick={{fill:'#707a8a',fontSize:11}} axisLine={{stroke:'#2b3139'}}/><YAxis tick={{fill:'#707a8a',fontSize:11}} axisLine={{stroke:'#2b3139'}} unit="%"/><Tooltip content={<Tip/>}/><Line type="monotone" dataKey="value" stroke="#0ecb81" strokeWidth={2.5} dot={{fill:'#0ecb81',r:4}} name="ROI %"/></LineChart>
</ResponsiveContainer>
</div>
</div>
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
<div className="flex items-center justify-between mb-4">
<div><h3 className="text-title-sm">Retention Cohorts</h3><p className="text-caption text-muted mt-0.5">Weekly cohort heatmap — FlowState launched Mar 31</p></div>
</div>
<table className="w-full"><thead><tr className="border-b border-hairline-dark">
<th className="text-left text-caption text-muted uppercase px-4 py-2">Cohort</th>
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 1</th>
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 7</th>
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 14</th>
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 30</th>
<th className="text-center text-caption text-muted uppercase px-4 py-2">Day 60</th>
</tr></thead><tbody>{cohorts.map(c => (
<tr key={c.week} className="border-b border-hairline-dark/50">
<td className="px-4 py-2"><div className="flex items-center gap-2"><Clock className="w-3.5 h-3.5 text-muted"/><span className="text-body-sm text-muted font-mono">{c.week}</span></div></td>
{[c.d1,c.d7,c.d14,c.d30,c.d60].map((v,i) => (
<td key={i} className="px-4 py-2 text-center"><span className={cn('inline-block min-w-[48px] px-2 py-1 rounded font-mono text-num-sm tabular-nums', getColor(v))}>{v===0?'—':v+'%'}</span></td>
))}
</tr>
))}</tbody></table>
<div className="mt-4 p-3 rounded-lg bg-trading-up/5 border border-trading-up/20">
<p className="text-body-sm text-trading-up">📈 <strong>FlowState Impact:</strong> Cohorts after Mar 31 show +7pp higher day-7 retention and +11pp higher day-14 retention vs pre-launch.</p>
</div>
</div>
<div className="rounded-xl bg-surface-card border border-hairline-dark p-5">
<h3 className="text-title-sm mb-4">Torque Custom Events Breakdown</h3>
<div className="space-y-4">{eventBreakdown.map(e => (
<div key={e.event} className="space-y-1.5">
<div className="flex items-center justify-between"><code className="font-mono text-caption text-muted">{e.event}</code><span className="font-mono text-num-sm text-[#eaecef] tabular-nums">{fmtNum(e.count)}</span></div>
<div className="h-2 rounded-full bg-surface-elevated overflow-hidden"><div className="h-full rounded-full transition-all duration-700" style={{width:(e.count/maxEvt*100)+'%',backgroundColor:e.color}}/></div>
</div>
))}</div>
</div>
</div>
)
}