File size: 23,018 Bytes
7200823
de40b1a
 
 
f667d47
c6b6c96
9e22644
f667d47
c6b6c96
f667d47
7200823
9e22644
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de40b1a
c6b6c96
 
 
 
 
 
 
 
 
 
 
7200823
f667d47
 
 
 
 
 
 
 
 
 
 
 
7200823
c6b6c96
de40b1a
 
c6b6c96
 
 
 
 
f667d47
 
 
c6b6c96
de40b1a
 
 
c6b6c96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de40b1a
7200823
 
f667d47
 
 
 
 
 
 
 
 
 
c6b6c96
 
 
 
 
 
f667d47
 
 
 
 
 
c6b6c96
 
 
 
 
 
 
 
 
 
 
 
f667d47
c6b6c96
 
f667d47
 
c6b6c96
 
 
 
de40b1a
9e22644
 
7200823
de40b1a
c6b6c96
7200823
c6b6c96
7200823
 
 
de40b1a
 
c6b6c96
 
 
 
de40b1a
c6b6c96
de40b1a
 
 
7200823
de40b1a
 
 
 
 
 
 
 
c6b6c96
 
 
 
 
f667d47
 
 
c6b6c96
f667d47
c6b6c96
 
 
 
 
 
f667d47
 
 
 
 
 
 
c6b6c96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f667d47
 
c6b6c96
f667d47
 
 
c6b6c96
f667d47
c6b6c96
f667d47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6b6c96
 
 
 
7200823
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
'use client'
import { cn, fmtUsd, shortAddr } from '@/lib/utils'
import { wallets } from '@/lib/mock-data'
import { RiskBadge } from '@/components/ui/RiskBadge'
import { RecoveryCard } from '@/components/ui/RecoveryCard'
import { useToast } from '@/components/ui/Toast'
import { Search, Flame, ExternalLink, ChevronRight, Zap, CheckCircle2, Loader2, Siren, Share2, AlertTriangle, Radio } from 'lucide-react'
import { useState, useCallback, useMemo } from 'react'
import type { ChurnRisk, Wallet } from '@/lib/types'
import { classifyWalletType } from '@/lib/agent-engine'

// --- Live Wallet Analyzer (Helius) ---
interface LiveResult {
  address: string; score: number; risk: string; detectedSignals: string[]
  signals: { daysInactive: number; protocols: string[]; currentStreak: number; volumeDropPct: number; totalTxLast30d: number; lastActiveDaysAgo: string }
  torque: { fired: boolean; eventId?: string; eventName?: string }
}

function LiveAnalyzer() {
  const [addr, setAddr] = useState('')
  const [loading, setLoading] = useState(false)
  const [result, setResult] = useState<LiveResult | null>(null)
  const [err, setErr] = useState<string | null>(null)
  const [autoFire, setAutoFire] = useState(false)
  const { fire: toast } = useToast()

  const analyze = useCallback(async () => {
    if (!addr.trim() || loading) return
    setLoading(true); setErr(null); setResult(null)
    try {
      const res = await fetch('/api/helius/analyze', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ address: addr.trim(), autoFire }),
      })
      const data = await res.json()
      if (!res.ok) { setErr(data.error); return }
      setResult(data)
      if (data.torque?.fired) {
        toast({ type: 'event', title: `${data.torque.eventName}${addr.slice(0,8)}...`, body: data.torque.eventId?.slice(0,10) })
      }
    } catch (e: any) {
      setErr(e.message)
    } finally {
      setLoading(false)
    }
  }, [addr, autoFire, loading, toast])

  const RISK_CLR: Record<string, string> = { critical:'text-trading-down', high:'text-[#ff9500]', medium:'text-brand-yellow', low:'text-trading-up', safe:'text-muted' }
  const RISK_BG: Record<string, string> = { critical:'border-trading-down/30 bg-trading-down/5', high:'border-[#ff9500]/30 bg-[#ff9500]/5', medium:'border-brand-yellow/30 bg-brand-yellow/5', low:'border-trading-up/30 bg-trading-up/5', safe:'border-hairline-dark bg-surface-elevated' }

  return (
    <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 gap-2">
        <Radio className="w-4 h-4 text-trading-up" />
        <h3 className="text-title-sm">Live Wallet Analyzer</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">HELIUS</span>
        <p className="text-caption text-muted ml-1">Enter any Solana address — real on-chain churn score</p>
      </div>
      <div className="p-5 space-y-4">
        <div className="flex gap-2">
          <input
            value={addr} onChange={e => setAddr(e.target.value)}
            onKeyDown={e => e.key === 'Enter' && analyze()}
            placeholder="Enter Solana wallet address..."
            className="flex-1 h-10 px-4 rounded-lg bg-surface-elevated border border-hairline-dark text-body-sm text-[#eaecef] placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-brand-yellow/50 font-mono"
          />
          <label className="flex items-center gap-2 px-3 rounded-lg border border-hairline-dark bg-surface-elevated cursor-pointer select-none">
            <input type="checkbox" checked={autoFire} onChange={e => setAutoFire(e.target.checked)} className="accent-brand-yellow" />
            <span className="text-caption text-muted whitespace-nowrap">Auto-fire Torque</span>
          </label>
          <button onClick={analyze} disabled={loading || !addr.trim()}
            className="flex items-center gap-2 px-4 py-2 rounded-lg bg-brand-yellow text-ink text-button font-semibold hover:bg-brand-yellow-active transition disabled:opacity-50">
            {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Zap className="w-4 h-4" />}
            {loading ? 'Scanning...' : 'Analyze'}
          </button>
        </div>

        {err && <p className="text-body-sm text-trading-down px-1">Error: {err}</p>}

        {result && (
          <div className={cn('rounded-xl border p-4 space-y-3', RISK_BG[result.risk] || 'border-hairline-dark')}>
            <div className="flex items-center justify-between">
              <div>
                <p className="font-mono text-body-sm text-[#eaecef]">{result.address.slice(0,8)}...{result.address.slice(-6)}</p>
                <a href={`https://solscan.io/account/${result.address}`} target="_blank" rel="noreferrer" className="text-caption text-muted hover:text-brand-yellow transition flex items-center gap-1 mt-0.5">
                  <ExternalLink className="w-3 h-3" />View on Solscan
                </a>
              </div>
              <div className="text-right">
                <p className={cn('font-mono text-display-sm font-bold tabular-nums', RISK_CLR[result.risk])}>{result.score}</p>
                <p className={cn('text-caption font-bold uppercase', RISK_CLR[result.risk])}>{result.risk}</p>
              </div>
            </div>
            <div className="grid grid-cols-4 gap-3">
              <div className="rounded-lg bg-surface-elevated p-2.5 text-center">
                <p className="text-[10px] text-muted">Inactive</p>
                <p className="font-mono text-body-sm text-[#eaecef] mt-0.5">{result.signals.lastActiveDaysAgo}</p>
              </div>
              <div className="rounded-lg bg-surface-elevated p-2.5 text-center">
                <p className="text-[10px] text-muted">Streak</p>
                <p className="font-mono text-body-sm text-[#eaecef] mt-0.5">{result.signals.currentStreak}d</p>
              </div>
              <div className="rounded-lg bg-surface-elevated p-2.5 text-center">
                <p className="text-[10px] text-muted">Vol drop</p>
                <p className="font-mono text-body-sm text-[#eaecef] mt-0.5">{result.signals.volumeDropPct}%</p>
              </div>
              <div className="rounded-lg bg-surface-elevated p-2.5 text-center">
                <p className="text-[10px] text-muted">30d txs</p>
                <p className="font-mono text-body-sm text-[#eaecef] mt-0.5">{result.signals.totalTxLast30d}</p>
              </div>
            </div>
            {result.signals.protocols.length > 0 && (
              <div className="flex flex-wrap gap-1">
                {result.signals.protocols.map(p => <span key={p} className="text-[10px] px-1.5 py-0.5 rounded bg-surface-elevated text-muted">{p}</span>)}
              </div>
            )}
            {result.detectedSignals.length > 0 && (
              <div className="space-y-1">
                {result.detectedSignals.map((s, i) => <p key={i} className="text-caption text-muted flex items-center gap-1.5"><span className="text-trading-down"></span>{s}</p>)}
              </div>
            )}
            {result.torque.fired && (
              <div className="flex items-center gap-2 p-2.5 rounded-lg bg-trading-up/10 border border-trading-up/30">
                <CheckCircle2 className="w-4 h-4 text-trading-up flex-shrink-0" />
                <div>
                  <p className="text-caption text-trading-up font-semibold">{result.torque.eventName} fired via Torque</p>
                  <p className="font-mono text-[10px] text-muted">{result.torque.eventId}</p>
                </div>
              </div>
            )}
          </div>
        )}
      </div>
    </div>
  )
}

type Filter = ChurnRisk | 'all'
const filters: Filter[] = ['all', 'critical', 'high', 'medium', 'low', 'safe']

const EVENT_MAP: Record<ChurnRisk, string> = {
  critical: 'churn_risk_high', high: 'churn_risk_high',
  medium: 'churn_risk_medium', low: 'inactivity_detected', safe: 'streak_maintained',
}
const ACTION_LABEL: Record<ChurnRisk, string> = {
  critical: 'Send Gift', high: 'Enter Raffle', medium: 'Activate Rebate',
  low: 'Flag Inactive', safe: 'Reward Streak',
}
const RISK_CAN_INTERVENE: ChurnRisk[] = ['critical', 'high', 'medium']

// Wallets with savedCount > 0 have prior rescues — show recovery card option
const RESCUED_WALLETS = new Set(wallets.filter(w => w.savedCount > 0).map(w => w.address))

// Pre-churn: "low" risk wallets with streak ≤ 2 and recent inactivity — early warning
function isPreChurnWarning(w: Wallet): boolean {
  const daysMatch = w.lastActive.match(/(\d+)d/)
  const days = daysMatch ? parseInt(daysMatch[1]) : 0
  const wtype = classifyWalletType(w.protocols)
  const threshold = wtype === 'trader' ? 3 : wtype === 'lp' ? 5 : 7
  return w.churnRisk === 'low' && days >= threshold && w.streak <= 2
}

export default function WalletsPage() {
  const { fire: toast } = useToast()
  const [f, setF] = useState<Filter>('all')
  const [q, setQ] = useState('')
  const [sort, setSort] = useState<'risk' | 'volume' | 'streak'>('risk')
  const [firing, setFiring] = useState<string | null>(null)
  const [fired, setFired] = useState<Map<string, string>>(new Map())
  const [bulkFiring, setBulkFiring] = useState(false)
  const [bulkProgress, setBulkProgress] = useState<{ done: number; total: number } | null>(null)
  const [recoveryWallet, setRecoveryWallet] = useState<Wallet | null>(null)

  const preChurnCount = useMemo(() => wallets.filter(isPreChurnWarning).length, [])

  const list = wallets
    .filter(w => f === 'all' || w.churnRisk === f)
    .filter(w => !q || w.address.toLowerCase().includes(q.toLowerCase()))
    .sort((a, b) => sort === 'risk' ? b.riskScore - a.riskScore : sort === 'volume' ? b.totalVolume - a.totalVolume : b.streak - a.streak)

  const criticalUnfired = wallets.filter(w =>
    (w.churnRisk === 'critical' || w.churnRisk === 'high') && !fired.has(w.address)
  )

  const intervene = useCallback(async (w: Wallet) => {
    if (firing || fired.has(w.address)) return
    setFiring(w.address)
    try {
      const res = await fetch('/api/torque/events', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          wallet: w.address,
          eventName: EVENT_MAP[w.churnRisk],
          data: { risk: w.churnRisk, score: w.riskScore, detectedBy: 'flowstate-dashboard' },
          risk: w.churnRisk,
          score: w.riskScore,
        }),
      })
      const data = await res.json()
      const eventId = data.eventId || 'sent'
      setFired(prev => new Map(prev).set(w.address, eventId))
      toast({
        type: 'event',
        title: `${ACTION_LABEL[w.churnRisk]}${shortAddr(w.address)}`,
        body: `${EVENT_MAP[w.churnRisk]} · ${eventId.slice(0, 10)}`,
      })
    } catch {
      setFired(prev => new Map(prev).set(w.address, 'error'))
      toast({ type: 'error', title: 'Event failed', body: w.address.slice(0, 12) })
    } finally {
      setFiring(null)
    }
  }, [firing, fired, toast])

  const bulkRescue = useCallback(async () => {
    if (bulkFiring || criticalUnfired.length === 0) return
    setBulkFiring(true)
    setBulkProgress({ done: 0, total: criticalUnfired.length })

    const targets = criticalUnfired.map(w => ({
      wallet: w.address,
      eventName: EVENT_MAP[w.churnRisk],
      risk: w.churnRisk,
      score: w.riskScore,
    }))

    try {
      const res = await fetch('/api/torque/bulk-fire', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ targets }),
      })
      const data = await res.json()

      if (data.details) {
        const newFired = new Map(fired)
        data.details.forEach((d: any, i: number) => {
          if (d.success) newFired.set(criticalUnfired[i].address, d.eventId || 'sent')
        })
        setFired(newFired)
        setBulkProgress({ done: data.fired, total: data.total })
        toast({
          type: data.fired > 0 ? 'success' : 'error',
          title: `Bulk rescue: ${data.fired}/${data.total} fired`,
          body: data.fired > 0 ? 'All critical wallets targeted via Torque' : data.details[0]?.error,
        })
      }
    } catch (e: any) {
      toast({ type: 'error', title: 'Bulk fire failed', body: e.message })
    } finally {
      setBulkFiring(false)
      setTimeout(() => setBulkProgress(null), 3000)
    }
  }, [bulkFiring, criticalUnfired, fired, toast])

  return (
    <div className="p-6 space-y-6">
      {recoveryWallet && (
        <RecoveryCard
          wallet={recoveryWallet.address}
          savedCount={recoveryWallet.savedCount}
          daysInactive={parseInt(recoveryWallet.lastActive.match(/(\d+)/)?.[1] || '8')}
          campaignName={recoveryWallet.churnRisk === 'critical' || recoveryWallet.churnRisk === 'high' ? 'Anti-Churn Gift Drop' : 'Streak Multiplier Rebate'}
          onClose={() => setRecoveryWallet(null)}
        />
      )}

      <div className="flex items-center justify-between">
        <div>
          <h1 className="text-display-sm text-[#eaecef]">Wallets</h1>
          <p className="text-body-md text-muted mt-1">Monitor wallet health, churn risk & activity patterns</p>
        </div>
        <div className="flex items-center gap-3">
          {preChurnCount > 0 && (
            <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-brand-yellow/10 border border-brand-yellow/30">
              <AlertTriangle className="w-4 h-4 text-brand-yellow" />
              <span className="text-caption text-brand-yellow font-semibold">{preChurnCount} pre-churn</span>
            </div>
          )}
          {fired.size > 0 && (
            <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-trading-up/10 border border-trading-up/30">
              <CheckCircle2 className="w-4 h-4 text-trading-up" />
              <span className="text-caption text-trading-up font-semibold">{fired.size} events sent</span>
            </div>
          )}
          {criticalUnfired.length > 0 && (
            <button
              onClick={bulkRescue}
              disabled={bulkFiring}
              className="flex items-center gap-2 px-4 py-2 rounded-md bg-trading-down/10 border border-trading-down/40 text-trading-down text-button font-semibold hover:bg-trading-down/20 transition disabled:opacity-50"
            >
              {bulkFiring ? <Loader2 className="w-4 h-4 animate-spin" /> : <Siren className="w-4 h-4" />}
              {bulkProgress
                ? `${bulkProgress.done}/${bulkProgress.total} fired`
                : bulkFiring ? 'Rescuing...'
                : `Rescue All Critical (${criticalUnfired.length})`}
            </button>
          )}
        </div>
      </div>

      <LiveAnalyzer />

      <div className="grid grid-cols-2 lg:grid-cols-6 gap-3">
        {filters.map(r => (
          <button key={r} onClick={() => setF(r)} className={cn('rounded-xl border p-3 text-center transition-all', f === r ? 'border-brand-yellow/40 bg-brand-yellow/5' : 'border-hairline-dark bg-surface-card hover:border-brand-yellow/20')}>
            <span className="text-caption text-muted capitalize">{r}</span>
            <p className="font-mono text-title-md text-[#eaecef] tabular-nums mt-0.5">{r === 'all' ? wallets.length : wallets.filter(w => w.churnRisk === r).length}</p>
          </button>
        ))}
      </div>

      <div className="flex items-center gap-3">
        <div className="relative flex-1 max-w-md">
          <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted" />
          <input type="text" placeholder="Search wallet..." value={q} onChange={e => setQ(e.target.value)} className="w-full h-9 pl-9 pr-4 rounded-lg bg-surface-card border border-hairline-dark text-body-sm placeholder:text-muted focus:outline-none focus:ring-1 focus:ring-brand-yellow/50" />
        </div>
        <div className="flex items-center gap-1 p-1 rounded-lg bg-surface-card border border-hairline-dark">
          {(['risk', 'volume', 'streak'] as const).map(s => <button key={s} onClick={() => setSort(s)} className={cn('px-3 py-1.5 rounded-md text-caption transition capitalize', sort === s ? 'bg-brand-yellow text-ink font-semibold' : 'text-muted hover:text-[#eaecef]')}>{s}</button>)}
        </div>
      </div>

      <div className="rounded-xl bg-surface-card border border-hairline-dark overflow-hidden">
        <table className="w-full"><thead><tr className="border-b border-hairline-dark bg-surface-elevated/50">
          <th className="text-left text-caption text-muted uppercase tracking-wider px-5 py-3">Wallet</th>
          <th className="text-center text-caption text-muted uppercase tracking-wider px-5 py-3">Risk</th>
          <th className="text-right text-caption text-muted uppercase tracking-wider px-5 py-3">Score</th>
          <th className="text-right text-caption text-muted uppercase tracking-wider px-5 py-3">Volume</th>
          <th className="text-right text-caption text-muted uppercase tracking-wider px-5 py-3">Streak</th>
          <th className="text-center text-caption text-muted uppercase tracking-wider px-5 py-3">Protocols</th>
          <th className="text-right text-caption text-muted uppercase tracking-wider px-5 py-3">Last Active</th>
          <th className="text-right text-caption text-muted uppercase tracking-wider px-5 py-3">Action</th>
        </tr></thead><tbody>{list.map(w => {
          const isFiring = firing === w.address
          const eventId = fired.get(w.address)
          const canIntervene = RISK_CAN_INTERVENE.includes(w.churnRisk)
          const preChurn = isPreChurnWarning(w)
          const rescued = RESCUED_WALLETS.has(w.address)
          const wtype = classifyWalletType(w.protocols)
          return (
            <tr key={w.address} className={cn('border-b border-hairline-dark/50 hover:bg-surface-hover transition-colors group', preChurn && 'bg-brand-yellow/3')}>
              <td className="px-5 py-4">
                <div className="flex items-center gap-2">
                  <span className="font-mono text-body-md text-[#eaecef]">{shortAddr(w.address)}</span>
                  <a href={`https://solscan.io/account/${w.address}`} target="_blank" rel="noreferrer">
                    <ExternalLink className="w-3 h-3 text-muted opacity-0 group-hover:opacity-100 transition" />
                  </a>
                  {preChurn && <span className="text-[9px] font-bold uppercase px-1.5 py-0.5 rounded bg-brand-yellow/15 text-brand-yellow border border-brand-yellow/30">⚡ pre-churn</span>}
                </div>
                <div className="flex items-center gap-2 mt-0.5">
                  <span className="text-[10px] text-muted capitalize">{wtype}</span>
                  {eventId && eventId !== 'error' && (
                    <p className="font-mono text-[10px] text-trading-up">✓ {eventId.slice(0, 10)}...</p>
                  )}
                </div>
              </td>
              <td className="px-5 py-4 text-center"><RiskBadge risk={w.churnRisk} /></td>
              <td className="px-5 py-4 text-right">
                <div className="flex items-center justify-end gap-2">
                  <div className="w-16 h-1.5 rounded-full bg-surface-elevated overflow-hidden">
                    <div className={cn('h-full rounded-full transition-all duration-700', w.riskScore >= 80 ? 'bg-trading-down' : w.riskScore >= 60 ? 'bg-[#ff9500]' : w.riskScore >= 40 ? 'bg-brand-yellow' : w.riskScore >= 20 ? 'bg-trading-up' : 'bg-brand-turquoise')} style={{ width: w.riskScore + '%' }} />
                  </div>
                  <span className="font-mono text-num-sm text-muted tabular-nums w-8 text-right">{w.riskScore}</span>
                </div>
              </td>
              <td className="px-5 py-4 text-right font-mono text-num-sm text-[#eaecef] tabular-nums">{fmtUsd(w.totalVolume)}</td>
              <td className="px-5 py-4 text-right">
                <div className="flex items-center justify-end gap-1">
                  <Flame className={cn('w-3.5 h-3.5', w.streak >= 30 ? 'text-brand-yellow' : w.streak >= 7 ? 'text-trading-up' : w.streak === 0 ? 'text-trading-down' : 'text-muted')} />
                  <span className="font-mono text-num-sm tabular-nums">{w.streak}d</span>
                </div>
              </td>
              <td className="px-5 py-4">
                <div className="flex flex-wrap justify-center gap-1">
                  {w.protocols.slice(0, 3).map(p => <span key={p} className="text-[10px] px-1.5 py-0.5 rounded bg-surface-elevated text-muted">{p}</span>)}
                  {w.protocols.length > 3 && <span className="text-[10px] px-1.5 py-0.5 rounded bg-surface-elevated text-muted">+{w.protocols.length - 3}</span>}
                </div>
              </td>
              <td className="px-5 py-4 text-right text-body-sm text-muted">{w.lastActive}</td>
              <td className="px-5 py-4 text-right">
                <div className="flex items-center justify-end gap-1.5">
                  {rescued && (
                    <button
                      onClick={() => setRecoveryWallet(w)}
                      className="inline-flex items-center gap-1 px-2 py-1 rounded-md bg-trading-up/10 border border-trading-up/30 text-trading-up text-[10px] font-semibold hover:bg-trading-up/20 transition"
                      title="Share recovery card"
                    >
                      <Share2 className="w-3 h-3" />
                    </button>
                  )}
                  {canIntervene && (
                    eventId ? (
                      <span className="inline-flex items-center gap-1 text-[10px] font-semibold text-trading-up">
                        <CheckCircle2 className="w-3 h-3" />Sent
                      </span>
                    ) : (
                      <button
                        onClick={() => intervene(w)}
                        disabled={!!firing}
                        className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-brand-yellow/10 border border-brand-yellow/30 text-brand-yellow text-[11px] font-semibold hover:bg-brand-yellow/20 transition disabled:opacity-40"
                      >
                        {isFiring ? <Loader2 className="w-3 h-3 animate-spin" /> : <Zap className="w-3 h-3" />}
                        {isFiring ? 'Firing...' : ACTION_LABEL[w.churnRisk]}
                      </button>
                    )
                  )}
                  {!canIntervene && !rescued && <ChevronRight className="w-4 h-4 text-muted opacity-0 group-hover:opacity-100 transition ml-auto" />}
                </div>
              </td>
            </tr>
          )
        })}</tbody></table>
      </div>
    </div>
  )
}