feat: cream terminal redesign, English-only UI, autopilot launch fixes

- Redesign dashboard into a cream-paper + vermilion IBM Plex Mono terminal
  (live L2 order book, cost/liq map, WS K-line, signal matrix, orchestration
  topology, risk radar, execution log, current positions, equity curve)
- Convert all user-facing UI and backend strings/prompts from Chinese to
  English (multi-language retained, default English)
- Add /api/statistics/full endpoint + full-stats frontend wiring
- Fix Autopilot launch: reuse the existing trader instead of creating
  duplicates (eliminates repeat ~35s create cost and stale-trader 404s);
  launch sends 5m scan interval
- Fix unreadable toasts: cream theme with high-contrast text + per-type accent
- Silence background dashboard polls (getTraderConfig) to stop error-toast spam
This commit is contained in:
tinkle-community
2026-06-30 16:03:52 +08:00
parent eba28bcf0e
commit 110bf52908
149 changed files with 6835 additions and 3611 deletions

View File

@@ -76,7 +76,7 @@ function buildAgentName(nowMs: number) {
return `${AGENT_NAME} valid_until ${nowMs + AGENT_VALIDITY_MS}`
}
const HYPERLIQUID_BUILDER_ADDRESS = '0x891dc6f05ad47a3c1a05da55e7a7517971faaf0d'
// 0.05% (5). Must match the server's defaultHyperliquidBuilderMaxFee and
// 0.05% (5 bps). Must match the server's defaultHyperliquidBuilderMaxFee and
// the BuilderInfo.Fee=50 (= 5 bps) used at order placement. The user signs
// this exact string when approving the builder during wallet connect.
const HYPERLIQUID_BUILDER_MAX_FEE = '0.05%'
@@ -131,7 +131,7 @@ function getPreferredWalletProvider(): WalletProvider | undefined {
function walletSupportLabel(language: Language) {
return language === 'zh'
? '支持 MetaMaskRabbyCoinbasePhantomBraveBackpackOKXTrust 等 EVM 钱包。'
? 'Supports MetaMask, Rabby, Coinbase, Phantom, Brave, Backpack, OKX, Trust and other EVM wallets.'
: 'Supports MetaMask, Rabby, Coinbase Wallet, Phantom, Brave, Backpack, OKX, Trust and other EVM wallets.'
}
@@ -241,53 +241,53 @@ export function HyperliquidWalletConnect({
const [hasWalletProvider, setHasWalletProvider] = useState(false)
const text = useMemo(
() => ({
title: language === 'zh' ? 'Hyperliquid 钱包' : 'Hyperliquid Wallet',
connect: language === 'zh' ? '连接 Hyperliquid' : 'Connect Hyperliquid',
connected: language === 'zh' ? '已连接' : 'Connected',
mainWallet: language === 'zh' ? 'EVM 主钱包' : 'EVM main wallet',
title: language === 'zh' ? 'Hyperliquid Wallet' : 'Hyperliquid Wallet',
connect: language === 'zh' ? 'Connect Hyperliquid' : 'Connect Hyperliquid',
connected: language === 'zh' ? 'Connected' : 'Connected',
mainWallet: language === 'zh' ? 'EVM main wallet' : 'EVM main wallet',
generateAgent:
language === 'zh'
? '生成 NOFX Agent 钱包'
? 'Generate NOFX agent wallet'
: 'Generate NOFX agent wallet',
approveAgent:
language === 'zh' ? '授权 Agent 交易' : 'Authorize agent trading',
language === 'zh' ? 'Authorize agent trading' : 'Authorize agent trading',
approveBuilder:
language === 'zh' ? '完成交易授权' : 'Finalize trading authorization',
save: language === 'zh' ? '保存到 NOFX' : 'Save to NOFX',
done: language === 'zh' ? '流程已完成' : 'Flow complete',
balance: language === 'zh' ? 'Hyperliquid 余额' : 'Hyperliquid balance',
withdrawable: language === 'zh' ? '可用' : 'Withdrawable',
equity: language === 'zh' ? '权益' : 'Equity',
marginUsed: language === 'zh' ? '已用保证金' : 'Margin used',
unrealizedPnl: language === 'zh' ? '未实现盈亏' : 'Unrealized PnL',
refresh: language === 'zh' ? '刷新' : 'Refresh',
language === 'zh' ? 'Finalize trading authorization' : 'Finalize trading authorization',
save: language === 'zh' ? 'Save to NOFX' : 'Save to NOFX',
done: language === 'zh' ? 'Flow complete' : 'Flow complete',
balance: language === 'zh' ? 'Hyperliquid balance' : 'Hyperliquid balance',
withdrawable: language === 'zh' ? 'Withdrawable' : 'Withdrawable',
equity: language === 'zh' ? 'Equity' : 'Equity',
marginUsed: language === 'zh' ? 'Margin used' : 'Margin used',
unrealizedPnl: language === 'zh' ? 'Unrealized PnL' : 'Unrealized PnL',
refresh: language === 'zh' ? 'Refresh' : 'Refresh',
noCustody:
language === 'zh'
? '资金保留在你的 Hyperliquid 账户NOFX 只保存已授权 Agent 钱包。'
? 'Funds stay in your Hyperliquid account; NOFX only stores the authorized agent wallet.'
: 'Funds stay in your Hyperliquid account; NOFX only stores the authorized agent wallet.',
agentExpiry:
language === 'zh' ? 'Agent 授权到期' : 'Agent authorization expires',
agentExpired: language === 'zh' ? '已过期' : 'Expired',
language === 'zh' ? 'Agent authorization expires' : 'Agent authorization expires',
agentExpired: language === 'zh' ? 'Expired' : 'Expired',
agentNoAuth:
language === 'zh'
? '未检测到 NOFX Agent 授权'
? 'No NOFX agent authorization found'
: 'No NOFX agent authorization found',
renewAgent:
language === 'zh'
? '续期 Agent 授权(+180 天)'
? 'Renew agent authorization (+180d)'
: 'Renew agent authorization (+180d)',
renewHint:
language === 'zh'
? 'Hyperliquid 不允许重复使用同一个 Agent续期会生成一个新的 Agent 并以 180 天有效期授权,然后自动更新 NOFX 保存的私钥(需登录)。'
? 'Hyperliquid forbids reusing an agent, so renewal creates a new agent approved for 180 days, then updates the stored key in NOFX (sign-in required).'
: 'Hyperliquid forbids reusing an agent, so renewal creates a new agent approved for 180 days, then updates the stored key in NOFX (sign-in required).',
noWalletTitle:
language === 'zh' ? '未检测到 EVM 钱包' : 'No EVM wallet detected',
language === 'zh' ? 'No EVM wallet detected' : 'No EVM wallet detected',
noWalletDetail:
language === 'zh'
? '先安装 Rabby MetaMask,创建或导入钱包,然后回到这里连接 Hyperliquid'
? 'Install Rabby or MetaMask, create or import a wallet, then return here to connect Hyperliquid.'
: 'Install Rabby or MetaMask, create or import a wallet, then return here to connect Hyperliquid.',
installRabby: language === 'zh' ? '安装 Rabby' : 'Install Rabby',
installMetaMask: language === 'zh' ? '安装 MetaMask' : 'Install MetaMask',
installRabby: language === 'zh' ? 'Install Rabby' : 'Install Rabby',
installMetaMask: language === 'zh' ? 'Install MetaMask' : 'Install MetaMask',
}),
[language]
)
@@ -500,7 +500,7 @@ export function HyperliquidWalletConnect({
if (!provider) {
setError(
language === 'zh'
? '未检测到 EVM 钱包,请安装 MetaMask / Rabby / OKX / Coinbase Wallet'
? 'No EVM wallet detected. Install MetaMask, Rabby, OKX or Coinbase Wallet.'
: 'No EVM wallet detected. Install MetaMask, Rabby, OKX or Coinbase Wallet.'
)
return
@@ -625,7 +625,7 @@ export function HyperliquidWalletConnect({
if (!isLoggedIn) {
setError(
language === 'zh'
? '续期需要先登录 NOFXHyperliquid 不允许重复使用同一个 Agent续期会生成新的 Agent 并更新已保存的私钥。'
? 'Renewal requires signing in: Hyperliquid forbids reusing the same agent, so renewal creates a new agent and updates the stored key.'
: 'Renewal requires signing in: Hyperliquid forbids reusing the same agent, so renewal creates a new agent and updates the stored key.'
)
return
@@ -680,7 +680,7 @@ export function HyperliquidWalletConnect({
}))
throw new Error(
language === 'zh'
? '新 Agent 已授权,但未找到对应的 NOFX 配置,请用“保存到 NOFX”手动保存。'
? 'New agent approved, but no matching NOFX config was found. Use "Save to NOFX" to store it.'
: 'New agent approved, but no matching NOFX config was found. Use "Save to NOFX" to store it.'
)
}
@@ -712,7 +712,7 @@ export function HyperliquidWalletConnect({
}))
toast.success(
language === 'zh'
? 'Agent 已续期(新 Agent有效期 180 天)'
? 'Agent renewed (new agent, valid 180 days)'
: 'Agent renewed (new agent, valid 180 days)'
)
await refreshAgentInfo()
@@ -721,7 +721,7 @@ export function HyperliquidWalletConnect({
err instanceof Error
? err.message
: language === 'zh'
? 'Agent 续期失败'
? 'Agent renewal failed'
: 'Agent renewal failed'
)
} finally {
@@ -773,14 +773,14 @@ export function HyperliquidWalletConnect({
: undefined,
}))
toast.success(
language === 'zh' ? '交易授权已完成' : 'Trading authorization finalized'
language === 'zh' ? 'Trading authorization finalized' : 'Trading authorization finalized'
)
} catch (err) {
setError(
err instanceof Error
? err.message
: language === 'zh'
? '交易授权失败'
? 'Trading authorization failed'
: 'Trading authorization failed'
)
} finally {
@@ -793,7 +793,7 @@ export function HyperliquidWalletConnect({
if (!isLoggedIn) {
setError(
language === 'zh'
? '请先登录 NOFX再保存 Agent 钱包用于交易。'
? 'Please sign in before saving the agent wallet for trading.'
: 'Please sign in before saving the agent wallet for trading.'
)
return
@@ -899,7 +899,7 @@ export function HyperliquidWalletConnect({
onClick={() => setOpen((value) => !value)}
className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-bold transition-all border ${
complete
? 'bg-emerald-500/10 border-emerald-400/30 text-emerald-300'
? 'bg-nofx-success/10 border-nofx-success/30 text-nofx-success'
: 'bg-nofx-gold/10 border-nofx-gold/30 text-nofx-gold hover:bg-nofx-gold/20'
}`}
>
@@ -913,11 +913,11 @@ export function HyperliquidWalletConnect({
{(open || inline) && (
<div
className={`${inline ? 'relative w-full' : 'absolute right-0 top-full mt-2 w-[420px] shadow-2xl shadow-black/50'} rounded-2xl border border-nofx-gold/20 bg-[#11151B] z-[80] overflow-hidden`}
className={`${inline ? 'relative w-full' : 'absolute right-0 top-full mt-2 w-[420px] shadow-2xl shadow-black/10'} rounded-2xl border border-[rgba(26,24,19,0.14)] bg-nofx-bg-lighter z-[80] overflow-hidden`}
>
<div className="flex items-center justify-between p-4 border-b border-white/10">
<div className="flex items-center justify-between p-4 border-b border-[rgba(26,24,19,0.14)]">
<div>
<div className="font-bold text-white">{text.title}</div>
<div className="font-bold text-nofx-text">{text.title}</div>
<div className="text-xs text-nofx-text-muted mt-1">
{text.noCustody}
</div>
@@ -929,7 +929,7 @@ export function HyperliquidWalletConnect({
<button
type="button"
onClick={() => setOpen(false)}
className="p-1 rounded hover:bg-white/10 text-zinc-500"
className="p-1 rounded hover:bg-[rgba(26,24,19,0.06)] text-nofx-text-muted"
>
<X className="w-4 h-4" />
</button>
@@ -943,10 +943,10 @@ export function HyperliquidWalletConnect({
<div
className={`w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold ${
step.status === 'done'
? 'bg-emerald-400 text-black'
? 'bg-nofx-success text-white'
: step.status === 'active'
? 'bg-nofx-gold text-black'
: 'bg-zinc-800 text-zinc-500'
? 'bg-nofx-gold text-white'
: 'bg-nofx-bg-deeper text-nofx-text-muted'
}`}
>
{step.status === 'done' ? (
@@ -958,8 +958,8 @@ export function HyperliquidWalletConnect({
<span
className={
step.status === 'pending'
? 'text-zinc-500'
: 'text-zinc-200'
? 'text-nofx-text-muted'
: 'text-nofx-text'
}
>
{step.label}
@@ -969,14 +969,14 @@ export function HyperliquidWalletConnect({
</div>
{error && (
<div className="rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-xs text-red-300">
<div className="rounded-lg border border-nofx-danger/30 bg-nofx-danger/10 p-3 text-xs text-nofx-danger">
{error}
</div>
)}
{!state.mainWallet && !hasWalletProvider && (
<div className="rounded-xl border border-nofx-gold/20 bg-nofx-gold/5 p-3">
<div className="text-sm font-semibold text-zinc-100">
<div className="text-sm font-semibold text-nofx-text">
{text.noWalletTitle}
</div>
<p className="mt-1 text-xs leading-5 text-nofx-text-muted">
@@ -987,7 +987,7 @@ export function HyperliquidWalletConnect({
href="https://rabby.io/"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-semibold text-zinc-200 hover:border-white/20 hover:bg-white/[0.07]"
className="inline-flex items-center gap-2 rounded-lg border border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper px-3 py-2 text-xs font-semibold text-nofx-text hover:border-[rgba(26,24,19,0.24)] hover:bg-nofx-bg"
>
<Download className="h-3.5 w-3.5" />
{text.installRabby}
@@ -996,7 +996,7 @@ export function HyperliquidWalletConnect({
href="https://metamask.io/download/"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-2 rounded-lg border border-white/10 bg-white/[0.04] px-3 py-2 text-xs font-semibold text-zinc-200 hover:border-white/20 hover:bg-white/[0.07]"
className="inline-flex items-center gap-2 rounded-lg border border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper px-3 py-2 text-xs font-semibold text-nofx-text hover:border-[rgba(26,24,19,0.24)] hover:bg-nofx-bg"
>
<ExternalLink className="h-3.5 w-3.5" />
{text.installMetaMask}
@@ -1005,14 +1005,14 @@ export function HyperliquidWalletConnect({
</div>
)}
<div className="rounded-xl border border-white/10 bg-black/25 p-3 space-y-2 text-xs">
<div className="rounded-xl border border-[rgba(26,24,19,0.14)] bg-nofx-bg-deeper p-3 space-y-2 text-xs">
{state.mainWallet && (
<div className="flex items-center justify-between gap-3">
<span className="text-zinc-500">Main</span>
<span className="text-nofx-text-muted">Main</span>
<button
type="button"
onClick={() => copy(state.mainWallet!, 'Main wallet')}
className="font-mono text-zinc-200 hover:text-nofx-gold flex items-center gap-1"
className="font-mono text-nofx-text hover:text-nofx-gold flex items-center gap-1"
>
{shortAddress(state.mainWallet)}{' '}
<Copy className="w-3 h-3" />
@@ -1021,11 +1021,11 @@ export function HyperliquidWalletConnect({
)}
{state.agentAddress && (
<div className="flex items-center justify-between gap-3">
<span className="text-zinc-500">Agent</span>
<span className="text-nofx-text-muted">Agent</span>
<button
type="button"
onClick={() => copy(state.agentAddress!, 'Agent wallet')}
className="font-mono text-zinc-200 hover:text-nofx-gold flex items-center gap-1"
className="font-mono text-nofx-text hover:text-nofx-gold flex items-center gap-1"
>
{shortAddress(state.agentAddress)}{' '}
<Copy className="w-3 h-3" />
@@ -1033,16 +1033,16 @@ export function HyperliquidWalletConnect({
</div>
)}
<div className="flex items-center justify-between gap-3">
<span className="text-zinc-500">Network</span>
<span className="font-mono text-zinc-300">
<span className="text-nofx-text-muted">Network</span>
<span className="font-mono text-nofx-text">
Hyperliquid Mainnet
</span>
</div>
{state.mainWallet && (
<div className="flex items-center justify-between gap-3 border-t border-white/10 pt-2">
<span className="text-zinc-500">{text.agentExpiry}</span>
<div className="flex items-center justify-between gap-3 border-t border-[rgba(26,24,19,0.14)] pt-2">
<span className="text-nofx-text-muted">{text.agentExpiry}</span>
{agentInfoLoading && !agentInfo ? (
<span className="font-mono text-zinc-400">Loading</span>
<span className="font-mono text-nofx-text-muted">Loading</span>
) : agentInfo ? (
(() => {
const { dateStr, daysLeft } = formatAgentExpiry(
@@ -1052,10 +1052,10 @@ export function HyperliquidWalletConnect({
const expired = daysLeft < 0
const soon = daysLeft >= 0 && daysLeft <= 14
const tone = expired
? 'text-red-300'
? 'text-nofx-danger'
: soon
? 'text-amber-300'
: 'text-zinc-200'
? 'text-nofx-gold'
: 'text-nofx-text'
return (
<span className={`font-mono text-right ${tone}`}>
{dateStr}
@@ -1066,7 +1066,7 @@ export function HyperliquidWalletConnect({
)
})()
) : (
<span className="font-mono text-zinc-500">
<span className="font-mono text-nofx-text-muted">
{text.agentNoAuth}
</span>
)}
@@ -1098,14 +1098,14 @@ export function HyperliquidWalletConnect({
{state.mainWallet && (
<div className="rounded-xl border border-nofx-gold/20 bg-nofx-gold/5 p-3 space-y-3 text-xs">
<div className="flex items-center justify-between gap-3">
<span className="font-bold text-zinc-100">
<span className="font-bold text-nofx-text">
{text.balance}
</span>
<button
type="button"
onClick={() => void refreshBalance()}
disabled={balanceLoading}
className="flex items-center gap-1 text-zinc-400 hover:text-nofx-gold disabled:opacity-60"
className="flex items-center gap-1 text-nofx-text-muted hover:text-nofx-gold disabled:opacity-60"
>
<RefreshCw
className={`w-3 h-3 ${balanceLoading ? 'animate-spin' : ''}`}
@@ -1114,37 +1114,37 @@ export function HyperliquidWalletConnect({
</button>
</div>
{balanceError ? (
<div className="rounded-lg border border-red-500/30 bg-red-500/10 p-2 text-red-300">
<div className="rounded-lg border border-nofx-danger/30 bg-nofx-danger/10 p-2 text-nofx-danger">
{balanceError}
</div>
) : (
<div className="grid grid-cols-2 gap-2">
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.withdrawable}</div>
<div className="mt-1 font-mono text-sm font-bold text-emerald-300">
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.withdrawable}</div>
<div className="mt-1 font-mono text-sm font-bold text-nofx-success">
{balanceLoading && !account
? 'Loading…'
: `${formatUSDC(account?.withdrawable)} USDC`}
</div>
</div>
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.equity}</div>
<div className="mt-1 font-mono text-sm font-bold text-zinc-100">
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.equity}</div>
<div className="mt-1 font-mono text-sm font-bold text-nofx-text">
{balanceLoading && !account
? 'Loading…'
: `${formatUSDC(account?.accountValue)} USDC`}
</div>
</div>
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.marginUsed}</div>
<div className="mt-1 font-mono text-sm font-bold text-zinc-100">
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.marginUsed}</div>
<div className="mt-1 font-mono text-sm font-bold text-nofx-text">
{formatUSDC(account?.totalMarginUsed)} USDC
</div>
</div>
<div className="rounded-lg bg-black/25 p-2">
<div className="text-zinc-500">{text.unrealizedPnl}</div>
<div className="rounded-lg bg-nofx-bg-deeper p-2">
<div className="text-nofx-text-muted">{text.unrealizedPnl}</div>
<div
className={`mt-1 font-mono text-sm font-bold ${(account?.unrealizedPnl ?? 0) >= 0 ? 'text-emerald-300' : 'text-red-300'}`}
className={`mt-1 font-mono text-sm font-bold ${(account?.unrealizedPnl ?? 0) >= 0 ? 'text-nofx-success' : 'text-nofx-danger'}`}
>
{formatSignedUSDC(account?.unrealizedPnl)} USDC
</div>
@@ -1192,7 +1192,7 @@ export function HyperliquidWalletConnect({
)}
{complete && (
<>
<div className="rounded-lg border border-emerald-400/30 bg-emerald-500/10 p-3 text-sm text-emerald-200 flex items-center gap-2">
<div className="rounded-lg border border-nofx-success/30 bg-nofx-success/10 p-3 text-sm text-nofx-success flex items-center gap-2">
<Shield className="w-4 h-4" /> {text.done}
</div>
<button
@@ -1201,26 +1201,26 @@ export function HyperliquidWalletConnect({
className="w-full flex items-center justify-center gap-2 rounded-xl border border-nofx-gold/30 bg-nofx-gold/10 px-4 py-3 text-sm font-bold text-nofx-gold transition hover:bg-nofx-gold/20"
>
{language === 'zh'
? '重新授权交易'
? 'Re-authorize trading'
: 'Re-authorize trading'}
</button>
</>
)}
</div>
<div className="flex items-center justify-between pt-2 border-t border-white/10">
<div className="flex items-center justify-between pt-2 border-t border-[rgba(26,24,19,0.14)]">
<a
href="https://app.hyperliquid.xyz/"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-zinc-500 hover:text-nofx-gold flex items-center gap-1"
className="text-xs text-nofx-text-muted hover:text-nofx-gold flex items-center gap-1"
>
Open Hyperliquid <ExternalLink className="w-3 h-3" />
</a>
<button
type="button"
onClick={resetFlow}
className="text-xs text-zinc-500 hover:text-red-300"
className="text-xs text-nofx-text-muted hover:text-nofx-danger"
>
Reset
</button>
@@ -1246,7 +1246,7 @@ function ActionButton({
type="button"
disabled={busy}
onClick={onClick}
className="w-full flex items-center justify-center gap-2 rounded-xl bg-nofx-gold px-4 py-3 text-sm font-bold text-black transition hover:bg-yellow-400 disabled:opacity-60 disabled:cursor-not-allowed"
className="w-full flex items-center justify-center gap-2 rounded-xl bg-nofx-gold px-4 py-3 text-sm font-bold text-white transition hover:opacity-90 disabled:opacity-60 disabled:cursor-not-allowed"
>
{busy ? <Loader2 className="w-4 h-4 animate-spin" /> : null}
{label}