)
}
// ── Empty state for when nothing is selected ──────────────────────────────────
function SelectHint() {
return (
Click any marker on the chart or a post below to see details
)
}
// ── Main dashboard ─────────────────────────────────────────────────────────────
export default function DashboardClient({ initialPosts }: Props) {
const intlLocale = useLocale()
const isZh = false // i18n shelved — Chinese branches kept as dead code for future revival; see messages/zh.json
const { asset, setAsset, timeframe, setTimeframe: _setTimeframe, setLivePrice, setSubscribed, setBotReadiness, setHlApiKeySet, isSubscribed, hlApiKeySet, botReadiness, livePrices } = useDashboardStore()
function setTimeframe(tf: string) { _setTimeframe(tf as '5m' | '15m' | '1H' | '4H' | '1D' | '1W') }
const { address, isConnected } = useAccount()
const params = useParams()
const locale = (typeof params?.locale === 'string' ? params.locale : 'en')
const [posts, setPosts] = useState(initialPosts)
const [performance, setPerformance] = useState(undefined)
const [candles, setCandles] = useState([])
const [chartErr, setChartErr] = useState('')
const [chartReload, setChartReload] = useState(0)
const [selectedPostId, setSelectedPostId] = useState(null)
const [macro, setMacro] = useState(null)
// For 1D: show all posts from selected day
const [selectedDayPosts, setSelectedDayPosts] = useState(null)
useEffect(() => {
if (!isConnected || !address) {
setSubscribed(false)
setHlApiKeySet(false)
setBotReadiness('unknown')
setPerformance(undefined)
return
}
// Clear account-scoped bot state immediately when the connected wallet
// changes so a previous wallet's status never leaks into the next one.
setSubscribed(false)
setHlApiKeySet(false)
setBotReadiness('unknown')
getUserPublic(address.toLowerCase())
.then((user) => {
setSubscribed(user.active)
setHlApiKeySet(user.hl_api_key_set)
setBotReadiness(user.hl_api_key_set ? 'saved' : 'unknown')
})
.catch(() => {})
}, [address, isConnected, setSubscribed, setHlApiKeySet, setBotReadiness])
useEffect(() => {
if (!isConnected || !address) {
setPerformance(undefined)
return
}
let cancelled = false
;(async () => {
try {
const env = getCachedViewEnvelope('view_performance', address)
?? getCachedViewEnvelope('view_user', address)
if (!env) {
if (!cancelled) setPerformance(undefined)
return
}
const data = await getPerformance(address, env)
if (!cancelled) setPerformance(data)
} catch {
if (!cancelled) setPerformance(undefined)
}
})()
return () => { cancelled = true }
}, [address, isConnected])
usePriceSocket({
onPrice: (a, price) => setLivePrice(a, price),
onNewPost: (post) => setPosts((prev) => [post as TrumpPost, ...prev].slice(0, 500)),
})
useEffect(() => {
setCandles([])
setChartErr('')
getPrices(asset, timeframe)
.then(c => { setCandles(c); setChartErr('') })
.catch(e => setChartErr(e instanceof Error ? e.message : (isZh ? '价格数据加载失败' : 'Failed to load price data')))
}, [asset, timeframe, chartReload, isZh])
useEffect(() => {
let alive = true
function load() {
swrFetch(
'macro-snapshot',
10 * 60_000,
() => getMacroSnapshot(),
fresh => { if (alive) setMacro(fresh) },
)
.then((snap) => { if (alive) setMacro(snap) })
.catch(() => {})
}
load()
const id = setInterval(load, 10 * 60_000)
return () => { alive = false; clearInterval(id) }
}, [])
const selectedPost = posts.find(p => p.id === selectedPostId) ?? null
// Show actionable signals first (buy/short), then most recent hold/neutral.
// Cap at 8 total so the list doesn't get too long.
const actionable = posts.filter(p => p.signal === 'buy' || p.signal === 'short').slice(0, 4)
const recentOthers = posts.filter(p => p.signal !== 'buy' && p.signal !== 'short').slice(0, 4)
const recentPosts = [...actionable, ...recentOthers].slice(0, 8)
const lastCandle = candles[candles.length - 1]
// Live price beats the most recent candle close — the candle is only
// refreshed when the user changes asset/timeframe, so without WS the number
// is frozen at page load.
const livePrice = livePrices[asset]
const displayPrice = livePrice ?? lastCandle?.close ?? null
// 24-hour change. Picking by candle index is wrong (200×4H = 33 days).
// Find the candle whose `time` is closest to (now − 24h) and use its open.
const now = lastCandle ? lastCandle.time : Math.floor(Date.now() / 1000)
const target24h = now - 24 * 3600
let baseline24h: typeof lastCandle | undefined
if (candles.length) {
baseline24h = candles[0]
for (const c of candles) {
if (c.time <= target24h) baseline24h = c
else break
}
}
const priceChange =
displayPrice != null && baseline24h
? ((displayPrice - baseline24h.open) / baseline24h.open) * 100
: 0
const totalPosts = posts.length
const todayKey = new Date().toISOString().slice(0, 10)
const signalsToday = posts.filter(p => p.published_at?.slice(0, 10) === todayKey).length
const actionablePosts = posts.filter(p => p.signal === 'buy' || p.signal === 'short').length
const trumpActionable = posts.filter(p => (p.source || '') === 'truth' && (p.signal === 'buy' || p.signal === 'short')).length
const macroActionable = posts.filter(p => ((p.source || '') === 'btc_bottom_reversal' || (p.source || '') === 'funding_reversal') && (p.signal === 'buy' || p.signal === 'short')).length
const kolMentions = posts.filter(p => (p.source || '') === 'kol').length
const winRate = performance?.win_rate ?? 0
const netPnl = performance?.net_pnl_usd ?? 0
const hasPriceData = candles.length > 0
const hasPerformanceData = Boolean(performance)
const macroScore = macro?.composite_score ?? null
const macroRegime = macro?.regime_label ?? null
const macroPct = macroScore == null ? 50 : Math.max(0, Math.min(100, (macroScore + 100) / 2))
const macroTone =
macroScore == null ? 'neutral'
: macroScore > 15 ? 'bull'
: macroScore < -15 ? 'bear'
: 'neutral'
const macroSummary =
macroScore == null ? 'Daily macro composite not loaded yet.'
: macroTone === 'bull' ? 'Risk backdrop is supportive. Trend-following setups get more room.'
: macroTone === 'bear' ? 'Backdrop is defensive. Preserve size and expect cleaner downside moves.'
: 'Backdrop is mixed. Useful for context, not a blind directional trigger.'
return (
{isZh ? '信号总览' : 'Signal monitor'}
All four signal pipelines in one place — Trump posts, BTC macro
bottom, funding-rate extremes, and KOL talks-vs-trades — alongside
your auto-trader's live state.
Live feed
{/* Open positions — what's on the book right now. Renders only when
a subscribed wallet is connected, so guests see the normal feed. */}