// Admin screens: GodView, Approvals, KillSwitch, Settings, ExchangeBalance, AdminBotDetail. const REGIME_SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'BNBUSDT']; function _recommendationTone(rec) { if (!rec) return 'muted'; const r = rec.toUpperCase(); if (r.includes('MOMENTUM')) return 'pos'; if (r.includes('MEAN') || r.includes('VWAP')) return 'warn'; if (r.includes('BREAKOUT')) return 'warn'; if (r.includes('STAND') || r.includes('FLAT') || r.includes('ASIDE')) return 'neg'; return 'muted'; } function _volTone(vol) { if (vol === 'LOW') return 'pos'; if (vol === 'NORMAL') return 'warn'; if (vol === 'HIGH') return 'warn'; if (vol === 'EXTREME') return 'neg'; return 'muted'; } function RegimeCard() { const [symbol, setSymbol] = React.useState('BTCUSDT'); const [data, setData] = React.useState(null); const [loading, setLoading] = React.useState(false); const [err, setErr] = React.useState(null); const [refreshedAt, setRefreshedAt] = React.useState(null); const fetchSnap = React.useCallback(async (sym) => { setLoading(true); setErr(null); try { const d = await api.get('/api/admin/regime/' + encodeURIComponent(sym)); setData(d); setRefreshedAt(new Date()); } catch (e) { setErr(e.detail || e.message || 'fetch failed'); } finally { setLoading(false); } }, []); React.useEffect(() => { let alive = true; fetchSnap(symbol); const t = setInterval(() => { if (alive) fetchSnap(symbol); }, 5 * 60 * 1000); return () => { alive = false; clearInterval(t); }; }, [symbol, fetchSnap]); const recTone = data ? _recommendationTone(data.recommendation) : 'muted'; const volTone = data ? _volTone(data.vol_regime) : 'muted'; return (

Market regime

Live read from Bybit V5 · auto-refresh 5 min {refreshedAt && · last: {refreshedAt.toLocaleTimeString()}}
{err &&
Error: {err}
} {data && (
${fmtNum(data.price, 2)}
Trend {data.trend_score}/4 Vol {data.vol_regime} {data.recommendation}
{data.rationale}
)} {!data && !err && loading && (
Fetching regime…
)}
); } function RegimeMetric({ label, value }) { return (
{label}
{value}
); } function AdminGodView({ go }) { const [dash] = useApi('/api/dashboard/admin'); const [bots] = useApi('/api/bots'); const [snapshots, setSnapshots] = React.useState({}); const [bal, balActions] = useApi('/api/admin/exchange/balance'); // Per-bot scanner snapshot polling (admin overview) React.useEffect(() => { if (!bots) return; let alive = true; const fetchAll = async () => { const out = {}; await Promise.all(bots.filter(b => b.is_active && !b.is_kill_switched).map(async (b) => { try { out[b.id] = await api.get(`/api/admin/bots/${b.id}/scanner`); } catch {} })); if (alive) setSnapshots(out); }; fetchAll(); const t = setInterval(fetchAll, 15000); return () => { alive = false; clearInterval(t); }; }, [bots]); return ( <>
Admin · God view

Command center

{dash && All systems live}
= 0} neg={dash?.platform_pnl < 0}/>
{/* Bybit wallet vs platform AUM reconciliation. The platform's internal accounting (Live AUM + Company Reserve) is bookkeeping — Bybit Wallet Balance is the real exchange total. The gap is UNINVESTED CASH on the exchange that hasn't been allocated to any bot via the subscribe flow. */} {bal && (() => { const platformTotal = (dash?.total_aum || 0) + (dash?.company_reserve || 0); const bybitTotal = bal.total_margin_balance || bal.total_wallet_balance || 0; const diff = bybitTotal - platformTotal; const borderColor = Math.abs(diff) < 1 ? 'var(--pos)' : (diff > 0 ? 'var(--ink-3)' : 'var(--neg)'); const label = Math.abs(diff) < 1 ? 'Reconciled' : (diff > 0 ? 'Uninvested cash on exchange' : 'PLATFORM AUM > BYBIT — investigate'); return (
Exchange reconciliation {bal.testnet ? '(testnet)' : '(mainnet)'}
Bybit wallet {USD(bybitTotal)}
Platform (AUM + Reserve) {USD(platformTotal)}
Diff {USD(diff, { sign: true })}
{label}
); })()} {(dash?.paper_active_bots || 0) > 0 && (
Paper-trading totals (simulated, not real money)
AUM {USD(dash?.paper_aum)}
PnL = 0 ? 'var(--pos)' : 'var(--neg)' }}> {USD(dash?.paper_pnl, { sign: true })}
Bots {dash?.paper_active_bots}
Paper
)}

Pending queue

Deposits, withdrawals, and KYC awaiting your review.

Bot scanner activity

Live per-bot status · auto-refreshes every 15s
{(bots || []).map(b => { const snap = snapshots[b.id]; const top = snap?.top_candidates?.[0]; const ageS = snap?.last_scan_at ? Math.round((Date.now() - new Date(snap.last_scan_at).getTime()) / 1000) : null; return ( go('admin-bot:' + b.id)}> ); })}
BotUnit priceAUMStatusLast scanTop candidate
{emojiFor(b.risk_category)}
{b.name} {b.paper_mode && PAPER}
{b.risk_category}
${fmtNum(b.unit_price, 4)} {USD(b.current_value, { decimals: 2 })} {b.is_kill_switched ? 'KILLED' : b.is_active ? 'Active' : 'Off'} {ageS != null ? ageS + 's ago' : '—'} {top ? ( {top.symbol} · = 0.70 ? 'num' : 'num muted'} style={(top.score ?? 0) >= 0.70 ? { color: 'var(--pos)' } : {}}>{(top.score ?? 0).toFixed(2)} ) : }
); } function QueueStat({ n, l }) { return (
{l}
{n}
); } function ExchangeCard({ bal, reload }) { if (!bal) return
Loading exchange balance…
; if (bal._error || bal.detail) { return (

Exchange balance

{typeof bal.detail === 'string' ? bal.detail : bal._error || 'Connection failed'}
); } return (

Bybit balance

{bal.testnet ? 'TESTNET' : 'MAINNET'}
{USD(bal.total_wallet_balance, { decimals: 4 })}
= 0 ? 'pos' : 'neg')}> {bal.total_unrealized_pnl >= 0 ? : } {USD(bal.total_unrealized_pnl, { sign: true, decimals: 4 })}
Available
{USD(bal.available_balance, { decimals: 4 })}
Margin
{USD(bal.total_margin_balance, { decimals: 4 })}
Open positions
{bal.open_positions_count}
); } /* ---------- Approvals ---------- */ function Approvals() { const [items, { reload: reloadTx }] = useApi('/api/transactions/pending'); const [pendingUsers, { reload: reloadUsers }] = useApi('/api/admin/users?approval_status=pending'); const [filter, setFilter] = React.useState('ALL'); const txs = items || []; const users = pendingUsers || []; const filtered = txs.filter(i => filter === 'ALL' || i.transaction_type.toUpperCase() === filter); const showUsers = filter === 'ALL' || filter === 'SIGNUP'; const visibleUsers = showUsers ? users : []; const totalPending = txs.length + users.length; const hasPendingDeposit = txs.some(i => i.transaction_type === 'deposit'); const approveTx = async (id) => { await api.put(`/api/transactions/${id}/approve`); reloadTx(); }; const rejectTx = async (id) => { const reason = prompt('Rejection reason:'); if (reason == null) return; await api.put(`/api/transactions/${id}/reject?reason=${encodeURIComponent(reason)}`); reloadTx(); }; const approveUser = async (id) => { await api.post(`/api/admin/users/${id}/approve`); reloadUsers(); }; const rejectUser = async (id) => { const reason = prompt('Rejection reason:'); if (reason == null) return; await api.post(`/api/admin/users/${id}/reject?reason=${encodeURIComponent(reason)}`); reloadUsers(); }; return ( <>
Admin · Queue

Approvals · {totalPending} pending

{['ALL','SIGNUP','DEPOSIT','WITHDRAWAL'].map(f => ( ))}
{hasPendingDeposit && filter !== 'WITHDRAWAL' && filter !== 'SIGNUP' && } {/* Pending user signups (with KYC details) */} {visibleUsers.map(u => { const kycReady = u.kyc_status === 'pending' && !!u.ktp_number; return (
{u.full_name || u.username} signup {kycReady ? KYC submitted : KYC missing}
{u.email} · @{u.username} · joined {fmtDateTime(u.created_at)}
{/* KYC details strip — only when submitted */} {kycReady && (
Full name (KTP)
{u.ktp_full_name || '—'}
NIK
{u.ktp_number || '—'}
Submitted
{u.kyc_submitted_at ? fmtDateTime(u.kyc_submitted_at) : '—'}
)} {!kycReady && (
This user hasn't filled in KYC yet. They'll be redirected to the KYC form when they sign in; approval is blocked until they submit.
)}
); })} {/* Pending transactions */} {filtered.map(item => (
{item.transaction_type === 'deposit' ? : }
User #{item.user_id} {item.transaction_type}
#{item.id} · {fmtDateTime(item.created_at)}{item.description ? ' · ' + item.description : ''}
{USD(item.amount)}
))} {filtered.length === 0 && visibleUsers.length === 0 && items && pendingUsers && (
Inbox zero
Nothing waiting in this queue.
)}
); } /* Deposit verification — shows the live Bybit USDT wallet balance + recent on-chain deposit history so admin can match a pending request to the actual incoming transfer before approving. Pure read-only — approval itself goes through the existing per-row Approve button. */ function DepositVerifyPanel() { const [bal, balActions] = useApi('/api/admin/exchange/balance'); const [deps, depActions] = useApi('/api/admin/exchange/deposits/recent'); const refresh = () => { balActions.reload(); depActions.reload(); }; const usdt = (bal?.assets || []).find(a => a.asset === 'USDT'); return (
Verify on Bybit
Check that the on-chain transfer landed before approving.
USDT wallet
{usdt ? USD(usdt.wallet_balance) : '—'}
total balance now
Available
{usdt ? USD(usdt.available_balance) : '—'}
not in open positions
Unrealized PnL
= 0 ? 'var(--pos)' : 'var(--neg)' }}> {usdt ? USD(usdt.unrealized_pnl, { sign: true }) : '—'}
{bal?.open_positions_count ?? 0} open position(s)
Recent USDT deposits (Bybit)
{!deps &&
Loading…
} {deps && deps.deposits && deps.deposits.length === 0 && (
No deposit records returned by Bybit yet.
)} {deps && deps.deposits && deps.deposits.length > 0 && ( {deps.deposits.map(d => ( ))}
WhenAmountChainStatusTx
{d.success_at ? new Date(d.success_at).toLocaleString() : '—'} {USD(d.amount)} {d.chain} {d.status} {d.tx_id || '—'}
)}
); } /* ---------- KillSwitch / Bot Control ---------- */ function KillSwitch({ go }) { const [bots, { reload }] = useApi('/api/bots'); const toggle = async (id) => { await api.post(`/api/bots/${id}/toggle-kill-switch`); reload(); }; const activeCount = (bots || []).filter(b => b.is_active && !b.is_kill_switched).length; return ( <>
Admin · Risk

Bot control

{activeCount}/{(bots || []).length} active
{(bots || []).map(b => (
{emojiFor(b.risk_category)}
{b.name}
{b.risk_category} · {USD(b.current_value, { decimals: 2 })}
toggle(b.id)}/>
{b.is_kill_switched ? 'KILLED' : b.is_active ? 'Active' : 'Off'}
))}
); } /* ---------- Settings (Platform Settings) ---------- */ function Settings() { const [list, { reload }] = useApi('/api/admin/settings'); return ( <>
Admin · Config

Platform settings

{(list || []).map(s => )}
); } function PerformanceFeeCard() { const [data, { reload, loading }] = useApi('/api/admin/settings/performance-fee'); const [busy, setBusy] = React.useState({}); const [err, setErr] = React.useState(null); const [flash, setFlash] = React.useState(null); const sweep = async (botId) => { if (!confirm('Sweep accumulated performance fee for this bot? This zeroes the counter (placeholder — no company wallet credit yet).')) return; setBusy(b => ({ ...b, [botId]: true })); setErr(null); setFlash(null); try { const r = await api.post('/api/admin/settings/performance-fee/sweep', { bot_id: botId }); setFlash(`Swept ${r.swept_amount.toFixed(8)} USDT from bot ${botId}.`); reload(); } catch (e) { setErr(e.detail || e.message || 'sweep failed'); } setBusy(b => ({ ...b, [botId]: false })); }; return (
Company share
rate {data ? `${(data.rate * 100).toFixed(1)}%` : '—'} total accumulated {data ? data.total_accumulated.toFixed(8) : '—'} USDT
Rate is edited below as PERFORMANCE_FEE_RATE. Sweep zeroes the bot's counter — for now this is a placeholder; a future iteration will credit a company wallet record and emit an audit trail.
{err &&
{err}
} {flash &&
{flash}
} {loading && !data ?
Loading…
: (
{(data?.per_bot || []).map(b => (
{b.bot_name} · bot {b.bot_id}
{(b.accumulated_performance_fee ?? 0).toFixed(8)} USDT
))}
)}
); } function SettingRow({ s, onSaved }) { const [val, setVal] = React.useState( s.type === 'bool' ? String(!!s.current_value) : s.type === 'secret' ? '' : (s.current_value ?? '') ); const [saving, setSaving] = React.useState(false); const [error, setError] = React.useState(null); const save = async () => { if (s.type === 'secret' && val === '') { setError('Paste a value to set/replace this secret. Blank is a no-op.'); return; } setSaving(true); setError(null); try { await api.put(`/api/admin/settings/${encodeURIComponent(s.key)}`, { value: String(val) }); onSaved(); } catch (e) { setError(e.detail || e.message); } setSaving(false); }; const sourceLabel = ({ db: { c:'pill accent', l:'db' }, env: { c:'pill', l:'env' }, default: { c:'pill', l:'default' } })[s.source] || { c:'pill', l:s.source }; return (
{s.key} {sourceLabel.l} {s.type === 'secret' && {s.is_set ? 'set' : 'unset'}} {s.requires_restart && restart}
{s.description}
{error &&
{error}
}
type: {s.type}
{s.type === 'bool' ? ( ) : s.type === 'secret' ? ( setVal(e.target.value)} placeholder={s.is_set ? '(set — paste to replace)' : '(unset)'} style={{ width: 280, fontSize: 13 }}/> ) : ( setVal(e.target.value)} style={{ width: 220, fontSize: 13 }}/> )}
); } /* ---------- Exchange Balance (dedicated page) ---------- */ function ExchangeBalance() { const [bal, { reload, loading }] = useApi('/api/admin/exchange/balance'); return ( <>
Admin · Exchange

Bybit account

{loading && !bal &&
Loading…
} {bal && } {bal && bal.assets && bal.assets.length > 0 && (

Assets

Per-coin breakdown
{bal.assets.map(a => ( ))}
AssetWalletAvailableMarginUnrealized
{a.asset} {fmtNum(a.wallet_balance, 4)} {fmtNum(a.available_balance, 4)} {fmtNum(a.margin_balance, 4)} = 0 ? 'var(--pos)' : 'var(--neg)', fontWeight: 600 }}>{a.unrealized_pnl >= 0 ? '+' : ''}{fmtNum(a.unrealized_pnl, 4)}
)}
); } /* ---------- Admin Bot Detail (tabs: positions, trades, performance, scanner, config, keys) ---------- */ function AdminBotDetail({ botId, go }) { const [bot] = useApi('/api/bots/' + botId); const [tab, setTab] = React.useState('positions'); if (!bot) return
Loading…
; const color = colorFor(bot.id); const flatten = async () => { if (!confirm('Flatten all open positions at market?')) return; try { const r = await api.post(`/api/admin/bots/${botId}/flatten`); alert(`Flattened ${r.closed_count} position(s)`); } catch (e) { alert('Flatten failed: ' + (e.detail || e.message)); } }; return ( <>
{emojiFor(bot.risk_category)}
{bot.risk_category} {bot.is_kill_switched ? 'KILLED' : bot.is_active ? 'Active' : 'Off'}

{bot.name}

{['positions','trades','performance','scanner','config','keys'].map(t => ( ))}
{tab === 'positions' && } {tab === 'trades' && } {tab === 'performance' && } {tab === 'scanner' && } {tab === 'config' && } {tab === 'keys' && }
); } function PositionsTab({ botId }) { const [positions] = useApi(`/api/admin/bots/${botId}/positions`); if (!positions) return
Loading…
; if (positions.length === 0) return
No open positions
; return (
{positions.map(p => ( ))}
IDSymbolSideQty remEntryMarkSLTP1/TP2UnrealizedStatus
#{p.id} {p.symbol} {p.side} {fmtNum(p.qty_remaining, 6)} {p.entry_px != null ? fmtNum(p.entry_px, 4) : '—'} {p.mark_price != null ? fmtNum(p.mark_price, 4) : '—'} {fmtNum(p.sl_px, 4)} {p.tp1_px != null ? fmtNum(p.tp1_px, 4) : '—'} / {p.tp2_px != null ? fmtNum(p.tp2_px, 4) : '—'} = 0 ? 'var(--pos)' : 'var(--neg)', fontWeight: 600 }}> {p.unrealized_pnl == null ? '—' : (p.unrealized_pnl >= 0 ? '+' : '') + fmtNum(p.unrealized_pnl, 4) + ' (' + (p.unrealized_pnl_pct != null ? p.unrealized_pnl_pct.toFixed(2) : '0.00') + '%)'} {p.status} {p.tp1_hit && TP1 Hit}
); } function TradesTab({ botId }) { const [trades] = useApi(`/api/admin/bots/${botId}/trades?limit=100`); if (!trades) return
Loading…
; if (trades.length === 0) return
No closed trades yet
; return (
{trades.map(t => ( ))}
IDSymbolSideQtyEntrySLTP1 / TP2RealizedFeesClosedReason
#{t.id} {t.symbol} {t.side} {fmtNum(t.qty, 6)} {t.entry_px != null ? fmtNum(t.entry_px, 4) : '—'} {t.sl_px != null ? fmtNum(t.sl_px, 4) : '—'} {t.tp1_px != null ? fmtNum(t.tp1_px, 4) : '—'} / {t.tp2_px != null ? fmtNum(t.tp2_px, 4) : '—'} = 0 ? 'var(--pos)' : 'var(--neg)', fontWeight: 600 }}>{t.realized_pnl >= 0 ? '+' : ''}{fmtNum(t.realized_pnl, 4)} {fmtNum(t.fees, 4)} {t.closed_at ? fmtDateTime(t.closed_at) : '—'} {t.close_reason || '—'} {t.tp1_hit && TP1 Hit}
); } function PerformanceTab({ botId, color }) { const [bucket, setBucket] = React.useState('hour'); const [points] = useApi(`/api/admin/bots/${botId}/unit-price-history?bucket=${bucket}`, [bucket]); const prices = (points || []).map(p => p.unit_price); const last = prices.length ? prices[prices.length - 1] : null; const first = prices.length ? prices[0] : null; const peak = prices.length ? Math.max(...prices) : null; const trough = prices.length ? Math.min(...prices) : null; const change = last && first ? last - first : 0; const changePct = first ? (change / first * 100) : 0; return ( <>
Unit price
${last != null ? fmtNum(last, 4) : '—'}
{[['hour','1H'],['day','1D'],['week','1W'],['month','1M']].map(([b, lab]) => ( ))}
= 0} neg={change < 0}/>
{prices.length > 0 ? :
No data yet at this resolution. Equity loop writes one snapshot per minute when the worker is running.
}
); } function ScannerTab({ botId }) { const [snap, { reload }] = useApi(`/api/admin/bots/${botId}/scanner`); React.useEffect(() => { const t = setInterval(reload, 15000); return () => clearInterval(t); }, [reload]); if (!snap) return
Loading…
; if (!snap.last_scan_at) return
No scan yet — worker may not be running, or first 3m candle hasn't closed since startup.
; const ageS = Math.round((Date.now() - new Date(snap.last_scan_at).getTime()) / 1000); return ( <>

Last scan

{snap.decision || '—'}
{ageS}s ago · trigger {snap.tick_symbol || '—'} · {snap.decision_reason || ''}

Candidates

Top {snap.top_candidates.length} by composite score
{snap.top_candidates.map((c, i) => ( ))}
SymbolSideScoreTrendMomentumPullbackBreakoutS/RVolRegime
{c.symbol} {c.side ? {c.side} : } = 0.7 ? 'var(--pos)' : (c.score ?? 0) >= 0.5 ? 'var(--warn)' : 'var(--ink-4)' }}>{(c.score ?? 0).toFixed(2)} {(c.trend ?? 0).toFixed(2)} {(c.momentum ?? 0).toFixed(2)} {(c.pullback ?? 0).toFixed(2)} {(c.breakout ?? 0).toFixed(2)} {(c.sr ?? 0).toFixed(2)} {(c.vol_regime ?? 0).toFixed(2)} {c.regime_ok ? : }
); } function ConfigTab({ botId }) { const [cfg, { reload, error: loadErr }] = useApi(`/api/admin/bots/${botId}/config`); const [draft, setDraft] = React.useState(null); const [saving, setSaving] = React.useState(false); const [error, setError] = React.useState(null); React.useEffect(() => { if (cfg) setDraft({ ...cfg, symbols_str: JSON.stringify(cfg.symbols || []), sp_str: JSON.stringify(cfg.strategy_params || {}, null, 2), so_str: JSON.stringify(cfg.symbol_overrides || {}, null, 2) }); }, [cfg]); if (loadErr) return
Failed to load config
{loadErr.detail || loadErr.message || 'Unknown error'}
; if (!draft) return
Loading…
; const save = async () => { setSaving(true); setError(null); try { let symbols, sp, so; try { symbols = JSON.parse(draft.symbols_str); } catch { setError('Symbols must be a JSON array'); setSaving(false); return; } try { sp = JSON.parse(draft.sp_str); } catch { setError('Strategy params must be valid JSON'); setSaving(false); return; } try { so = JSON.parse(draft.so_str || '{}'); } catch { setError('Symbol overrides must be valid JSON'); setSaving(false); return; } const body = { symbols, leverage: Number(draft.leverage), risk_per_trade_pct: Number(draft.risk_per_trade_pct), max_concurrent_positions: Number(draft.max_concurrent_positions), max_position_notional_pct: Number(draft.max_position_notional_pct), htf_interval: draft.htf_interval, ltf_interval: draft.ltf_interval, daily_loss_pct: Number(draft.daily_loss_pct), weekly_dd_pct: Number(draft.weekly_dd_pct), cooldown_seconds: Number(draft.cooldown_seconds), liq_buffer_pct: Number(draft.liq_buffer_pct), paper_mode: !!draft.paper_mode, enabled: !!draft.enabled, strategy_params: sp, symbol_overrides: so, }; await api.put(`/api/admin/bots/${botId}/config`, body); reload(); } catch (e) { setError(e.detail || e.message); } setSaving(false); }; const setF = (k, v) => setDraft({ ...draft, [k]: v }); return (
setF('symbols_str', e.target.value)} style={{ fontSize: 12 }}/>
setF('leverage', e.target.value)}/>
setF('risk_per_trade_pct', e.target.value)}/>
setF('max_concurrent_positions', e.target.value)}/>
setF('max_position_notional_pct', e.target.value)}/>
setF('htf_interval', e.target.value)}/> setF('ltf_interval', e.target.value)}/>
setF('daily_loss_pct', e.target.value)}/>
setF('weekly_dd_pct', e.target.value)}/>
setF('cooldown_seconds', e.target.value)}/>
setF('liq_buffer_pct', e.target.value)}/>