Files

300 lines
9.6 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>
)
}