Files
k 4c3c8c6f87 KOL count: 29 → 25 across marketing/SEO copy
Backend KOL_FEEDS trimmed from 29 to 25 (dead feeds removed).
Sync all hardcoded count mentions:
- layout.tsx JSON-LD, page.tsx (metric + comparison + copy)
- kol/page.tsx, KolPageClient.tsx ("and 26 more" → "and 22 more")
- glossary/page.tsx, opengraph-image.tsx
- public/llms.txt, llms-full.txt
- drop removed KOLs (Dragonfly Capital, Nic Carter) from named lists

Bundles other in-flight frontend work already in the working tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:55:27 +08:00

157 lines
6.6 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect, useCallback } from 'react'
import { useAccount } from 'wagmi'
import { useWsSubscribe } from '@/lib/wsContext'
interface TradeAlert {
id: number
event: 'execution_failed' | 'insufficient_balance' | 'budget_reached'
| 'reconcile_drift' | 'circuit_breaker_tripped'
asset?: string
reason?: string
balance_usd?: number
required_usd?: number
spent_usd?: number
cap_usd?: number
// reconcile_drift fields (from reconciler.py broadcast)
// orphan_hl is a list of {asset, trade_id} dicts, NOT a number
orphan_hl?: unknown[]
marked_closed?: number
ghost_positions?: unknown[]
// circuit_breaker_tripped fields (from circuit_breaker.py broadcast)
system?: string
cb_reason?: string
unlock_at?: string
}
// How long each alert stays visible before auto-dismissing.
const AUTO_DISMISS_MS = 10_000
function alertMessage(alert: TradeAlert): string {
switch (alert.event) {
case 'execution_failed':
return `Auto-trade failed${alert.asset ? ` on ${alert.asset}` : ''}${alert.reason ? `: ${alert.reason}` : ''}. Check your HL API key and account status.`
case 'insufficient_balance':
return `Insufficient balance to open${alert.asset ? ` ${alert.asset}` : ''}: account has $${alert.balance_usd?.toFixed(2) ?? '?'}, trade requires $${alert.required_usd?.toFixed(2) ?? '?'}. Top up your Hyperliquid account.`
case 'budget_reached':
return `Daily budget reached${alert.asset ? ` (${alert.asset})` : ''}: $${alert.spent_usd?.toFixed(0) ?? '?'} spent of $${alert.cap_usd?.toFixed(0) ?? '?'} cap. No more trades today.`
// B47: reconciler detects DB ↔ HL mismatch (ghost/orphan position)
case 'reconcile_drift': {
const parts = []
const orphanCount = (alert.orphan_hl as unknown[])?.length ?? 0
if (orphanCount > 0) parts.push(`${orphanCount} orphan(s) on HL`)
if (alert.marked_closed) parts.push(`${alert.marked_closed} auto-closed`)
if ((alert.ghost_positions as unknown[])?.length) parts.push(`${(alert.ghost_positions as unknown[]).length} ghost(s)`)
return `Position drift detected: ${parts.join(', ') || 'mismatch between DB and HL'}. Check your Hyperliquid account.`
}
// B53: circuit breaker fired — Auto-Trade suspended
case 'circuit_breaker_tripped':
return `Circuit breaker tripped${alert.system ? ` (${alert.system})` : ''}${alert.cb_reason ? `: ${alert.cb_reason}` : ''}. Auto-Trade suspended — turn it back ON on the Trump page to acknowledge and resume.`
default:
return 'Auto-trade event — check your account.'
}
}
let _seq = 0
export default function TradeAlertBanner() {
const { address } = useAccount()
const [alerts, setAlerts] = useState<TradeAlert[]>([])
const dismiss = useCallback((id: number) => {
setAlerts(prev => prev.filter(a => a.id !== id))
}, [])
// Auto-dismiss each alert after AUTO_DISMISS_MS.
// Effect re-runs whenever the alert list changes. For each alert we schedule
// a timeout; cleanup cancels all pending timers when the list updates or
// the component unmounts — prevents a dismissed-then-re-added alert from
// triggering a ghost dismiss.
useEffect(() => {
if (alerts.length === 0) return
const timers = alerts.map(a =>
window.setTimeout(() => dismiss(a.id), AUTO_DISMISS_MS)
)
return () => timers.forEach(clearTimeout)
}, [alerts, dismiss])
useWsSubscribe('trade_alert', useCallback((raw) => {
const msg = raw as { wallet?: string; event?: string } & Record<string, unknown>
if (!address || msg.wallet?.toLowerCase() !== address.toLowerCase()) return
const alert = { id: ++_seq, ...msg } as TradeAlert
setAlerts(prev => [alert, ...prev].slice(0, 3))
}, [address]))
// B47: reconcile_drift — DB↔HL position mismatch flagged by the reconciler.
useWsSubscribe('reconcile_drift', useCallback((raw) => {
const msg = raw as { wallet?: string } & Record<string, unknown>
if (!address || msg.wallet?.toLowerCase() !== address.toLowerCase()) return
const alert = { id: ++_seq, event: 'reconcile_drift' as const, ...msg } as TradeAlert
setAlerts(prev => [alert, ...prev].slice(0, 3))
}, [address]))
// B53: circuit_breaker_tripped — Auto-Trade suspended by the CB.
useWsSubscribe('circuit_breaker_tripped', useCallback((raw) => {
const msg = raw as { wallet?: string; reason?: string } & Record<string, unknown>
if (!address || msg.wallet?.toLowerCase() !== address.toLowerCase()) return
// Rename 'reason' → 'cb_reason' to avoid collision with the trade_alert 'reason' field.
const { reason: cb_reason, ...rest } = msg
const alert = { id: ++_seq, event: 'circuit_breaker_tripped' as const, cb_reason, ...rest } as TradeAlert
setAlerts(prev => [alert, ...prev].slice(0, 3))
}, [address]))
if (alerts.length === 0) return null
return (
<>
<style>{`
@keyframes tradeAlertIn {
from { opacity: 0; transform: translateX(-50%) translateY(-8px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes tradeAlertOut {
from { opacity: 1; }
to { opacity: 0; }
}
`}</style>
<div style={{
position: 'fixed', top: 60, left: '50%', transform: 'translateX(-50%)',
zIndex: 9999, display: 'flex', flexDirection: 'column', gap: 8,
width: 'min(480px, calc(100vw - 32px))',
pointerEvents: 'none',
animation: 'tradeAlertIn 0.2s ease',
}}>
{alerts.map(alert => (
<div key={alert.id} style={{
display: 'flex', alignItems: 'flex-start', gap: 10,
padding: '12px 14px', borderRadius: 10,
background: 'var(--bg)',
border: '1px solid var(--down)',
boxShadow: '0 4px 20px rgba(0,0,0,.3)',
pointerEvents: 'auto',
}}>
<span style={{ fontSize: 16, flexShrink: 0, marginTop: 1 }}></span>
<div style={{ flex: 1, fontSize: 12, lineHeight: 1.5, color: 'var(--ink)' }}>
<strong style={{ color: 'var(--down)', display: 'block', marginBottom: 2 }}>
Auto-trade blocked
</strong>
{alertMessage(alert)}
</div>
<button
onClick={() => dismiss(alert.id)}
style={{
flexShrink: 0, padding: 0, border: 'none', background: 'transparent',
cursor: 'pointer', color: 'var(--ink-4)', fontSize: 16, lineHeight: 1,
}}
aria-label="Dismiss"
>
×
</button>
</div>
))}
</div>
</>
)
}