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>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
'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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user