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

199 lines
8.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
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 } from 'react'
import { useWsSubscribe } from '@/lib/wsContext'
const API_BASE = '/api/proxy/api'
interface SignalAlert {
type: 'funding_signal'
symbol: string
time: string
close: number
tbr: number
vol_mult: number
bb_pct: number
bb_upper: number
btc_trend: string
enabled: boolean
}
function symbolLabel(s: string) {
return s.replace('USDT', '')
}
function timeAgo(iso: string) {
const diff = (Date.now() - new Date(iso).getTime()) / 1000
if (diff < 60) return `${Math.round(diff)}s ago`
if (diff < 3600) return `${Math.round(diff / 60)}m ago`
return `${Math.round(diff / 3600)}h ago`
}
// ── Main component ────────────────────────────────────────────────────────────
// Display-only: toggle is operator-only (requires X-Ingest-Key) so the
// on/off switch is intentionally absent from the user-facing UI.
export default function SignalMonitor() {
const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
const [enabled, setEnabledState] = useState(false)
const [signals, setSignals] = useState<SignalAlert[]>([])
const [btcTrend, setBtcTrend] = useState<string | null>(null)
const [lastScan, setLastScan] = useState<Date | null>(null)
// ── Load initial state ───────────────────────────────────────────────────
useEffect(() => {
fetch(`${API_BASE}/signal/status`)
.then(r => { if (!r.ok) throw new Error(); return r.json() })
.then(d => { setEnabledState(d.enabled) })
.catch(() => {})
fetch(`${API_BASE}/signal/history?limit=20`)
.then(r => r.json())
.then((list: SignalAlert[]) => {
if (!Array.isArray(list)) return
setSignals(list)
if (list.length > 0) {
setBtcTrend(list[0].btc_trend)
setLastScan(new Date(list[0].time))
}
})
.catch(() => {})
}, [])
// ── WebSocket listener (shared singleton connection via WsProvider) ─────────
useWsSubscribe('funding_signal', (msg) => {
const alert = msg as SignalAlert
setBtcTrend(alert.btc_trend)
setLastScan(new Date(alert.time))
setSignals(prev => [alert, ...prev].slice(0, 50))
})
// ── Render ───────────────────────────────────────────────────────────────
const btcUp = btcTrend?.includes('↑')
return (
<div className="card" style={{ padding: 20, marginTop: 12 }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 14 }}>
<div>
<div style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase', color: 'var(--ink-3)', marginBottom: 3 }}>
{isZh ? '突破监控' : 'Breakout Monitor'}
</div>
<div style={{ fontSize: 11, color: 'var(--ink-3)' }}>
{isZh ? 'ETH · LINK · 5 分钟扫描' : 'ETH · LINK · 5m scan'}
</div>
</div>
{/* Enabled status dot — display-only; toggle is operator-only (X-Ingest-Key) */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11, color: enabled ? '#22c55e' : 'var(--ink-3)' }}>
<div style={{
width: 7, height: 7, borderRadius: '50%',
background: enabled ? '#22c55e' : 'var(--ink-4)',
boxShadow: enabled ? '0 0 0 2px rgba(34,197,94,0.25)' : 'none',
}} />
{enabled ? (isZh ? '监控中' : 'Active') : (isZh ? '已暂停' : 'Paused')}
</div>
</div>
{/* Status row: BTC trend + last scan */}
<div style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
fontSize: 11, color: 'var(--ink-3)',
padding: '6px 10px', borderRadius: 8,
background: 'rgba(255,255,255,0.04)',
marginBottom: 14,
}}>
<span>
BTC &nbsp;
{btcTrend ? (
<strong style={{ color: btcUp ? '#22c55e' : '#ef4444' }}>{btcTrend}</strong>
) : (
<span style={{ opacity: 0.5 }}></span>
)}
</span>
<span>
{lastScan
? (isZh ? `最近信号 ${timeAgo(lastScan.toISOString())}` : `Last signal ${timeAgo(lastScan.toISOString())}`)
: enabled ? (isZh ? '扫描中…' : 'Scanning…') : (isZh ? '已暂停' : 'Paused')
}
</span>
</div>
{/* Signal list */}
{signals.length === 0 ? (
<div style={{
fontSize: 13, color: 'var(--ink-3)',
textAlign: 'center', padding: '20px 0',
lineHeight: 1.5,
}}>
{enabled
? (isZh ? '· 正在等待信号…' : '· Watching for signals…')
: (isZh ? '· 暂无历史信号' : '· No signals yet')}
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{signals.map((s) => (
<div key={`${s.symbol}-${s.time}`} style={{
padding: '11px 13px',
borderRadius: 10,
background: 'rgba(255,255,255,0.04)',
borderLeft: `3px solid ${s.enabled ? '#22c55e' : 'rgba(255,255,255,0.15)'}`,
}}>
{/* Top row */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{
fontWeight: 700, fontSize: 14,
fontFamily: 'var(--mono)',
color: 'var(--ink)',
}}>
{symbolLabel(s.symbol)}
</span>
{!s.enabled && (
<span style={{
fontSize: 9, padding: '2px 6px', borderRadius: 4,
background: 'rgba(255,255,255,0.08)',
color: 'var(--ink-3)',
textTransform: 'uppercase', letterSpacing: '0.05em',
}}>
silent
</span>
)}
</div>
<span style={{ fontSize: 11, color: 'var(--ink-3)' }}>
{timeAgo(s.time)}
</span>
</div>
{/* Metrics grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
{[
{ label: isZh ? '价格' : 'Price', value: `$${s.close.toLocaleString(undefined, { maximumFractionDigits: 2 })}`, color: 'var(--ink)' },
{ label: isZh ? '主动买入' : 'Taker Buy', value: `${(s.tbr * 100).toFixed(1)}%`, color: '#22c55e' },
{ label: isZh ? '成交量倍数' : 'Vol ×', value: `${s.vol_mult}×`, color: '#22c55e' },
].map(m => (
<div key={m.label} style={{
background: 'rgba(255,255,255,0.03)',
borderRadius: 6, padding: '5px 8px',
}}>
<div style={{ fontSize: 9, color: 'var(--ink-3)', marginBottom: 2, textTransform: 'uppercase', letterSpacing: '0.04em' }}>
{m.label}
</div>
<div style={{ fontSize: 13, fontFamily: 'var(--mono)', fontWeight: 600, color: m.color }}>
{m.value}
</div>
</div>
))}
</div>
{/* Footer */}
<div style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 8 }}>
{isZh ? `BB squeeze 处于第 ${s.bb_pct} 分位` : `BB squeeze ${s.bb_pct}th pct`} &nbsp;·&nbsp; {s.btc_trend}
</div>
</div>
))}
</div>
)}
</div>
)
}