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:
k
2026-06-09 22:55:27 +08:00
parent 9e0f6554cb
commit 4c3c8c6f87
57 changed files with 3464 additions and 1855 deletions
+156
View File
@@ -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>
</>
)
}