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

@@ -9,17 +9,17 @@ import { LanguageSwitcher } from '../common/LanguageSwitcher'
const labels = {
zh: {
welcome: '欢迎使用 NOFX',
subtitle: '创建账号开始使用',
email: '邮箱',
welcome: 'Welcome to NOFX',
subtitle: 'Create your account to get started',
email: 'Email',
emailPlaceholder: 'you@example.com',
password: '密码',
passwordPlaceholder: '至少 8 个字符',
passwordError: '密码至少需要 8 个字符',
submit: '开始使用',
submitting: '创建中...',
setupFailed: '创建失败,请重试',
singleUser: '单用户系统 — 这是唯一的账号',
password: 'Password',
passwordPlaceholder: 'At least 8 characters',
passwordError: 'Password must be at least 8 characters',
submit: 'Get Started',
submitting: 'Creating account...',
setupFailed: 'Setup failed, please try again',
singleUser: 'Single-user system — this is the only account',
},
en: {
welcome: 'Welcome to NOFX',
@@ -88,7 +88,7 @@ export function SetupPage() {
}
return (
<div className="relative min-h-screen w-full overflow-hidden bg-[#0a0a0f]">
<div className="relative min-h-screen w-full overflow-hidden bg-nofx-bg">
{/* Decorative background - simulates the main app behind a modal */}
{/* Grid */}
@@ -99,33 +99,33 @@ export function SetupPage() {
{/* Glow spots */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute top-[10%] left-[15%] w-[500px] h-[500px] bg-nofx-gold/8 rounded-full blur-[150px]" />
<div className="absolute bottom-[5%] right-[10%] w-[400px] h-[400px] bg-indigo-500/6 rounded-full blur-[140px]" />
<div className="absolute top-[40%] right-[30%] w-[300px] h-[300px] bg-emerald-500/4 rounded-full blur-[120px]" />
<div className="absolute bottom-[5%] right-[10%] w-[400px] h-[400px] bg-nofx-gold/6 rounded-full blur-[140px]" />
<div className="absolute top-[40%] right-[30%] w-[300px] h-[300px] bg-nofx-success/4 rounded-full blur-[120px]" />
</div>
{/* Faux UI elements in background to simulate the app */}
<div className="absolute inset-0 pointer-events-none opacity-[0.06]">
{/* Fake header bar */}
<div className="h-14 border-b border-white/20 flex items-center px-6 gap-4">
<div className="w-8 h-8 rounded-lg bg-white/40" />
<div className="h-3 w-20 rounded bg-white/30" />
<div className="h-3 w-16 rounded bg-white/20 ml-4" />
<div className="h-3 w-16 rounded bg-white/20" />
<div className="h-3 w-16 rounded bg-white/20" />
<div className="h-14 border-b border-nofx-text/20 flex items-center px-6 gap-4">
<div className="w-8 h-8 rounded-lg bg-nofx-text/40" />
<div className="h-3 w-20 rounded bg-nofx-text/30" />
<div className="h-3 w-16 rounded bg-nofx-text/20 ml-4" />
<div className="h-3 w-16 rounded bg-nofx-text/20" />
<div className="h-3 w-16 rounded bg-nofx-text/20" />
</div>
{/* Fake content cards */}
<div className="p-6 grid grid-cols-4 gap-4 mt-2">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 rounded-xl border border-white/15 bg-white/5" />
<div key={i} className="h-24 rounded-xl border border-nofx-text/15 bg-nofx-text/5" />
))}
</div>
<div className="px-6 mt-2">
<div className="h-64 rounded-xl border border-white/15 bg-white/5" />
<div className="h-64 rounded-xl border border-nofx-text/15 bg-nofx-text/5" />
</div>
</div>
{/* Blur overlay */}
<div className="absolute inset-0 backdrop-blur-md bg-black/60" />
<div className="absolute inset-0 backdrop-blur-md bg-nofx-bg/60" />
<LanguageSwitcher />
@@ -138,25 +138,25 @@ export function SetupPage() {
<div className="flex justify-center mb-4">
<div className="relative">
<div className="absolute -inset-4 bg-nofx-gold/20 rounded-full blur-2xl" />
<img src="/icons/nofx.svg" alt="NOFX" className="w-14 h-14 relative z-10 drop-shadow-[0_0_15px_rgba(240,185,11,0.3)]" />
<img src="/icons/nofx.svg" alt="NOFX" className="w-14 h-14 relative z-10" />
</div>
</div>
<h1 className="text-2xl font-bold text-white mb-1.5">{l.welcome}</h1>
<p className="text-zinc-500 text-sm">{l.subtitle}</p>
<h1 className="text-2xl font-bold text-nofx-text mb-1.5">{l.welcome}</h1>
<p className="text-nofx-text-muted text-sm">{l.subtitle}</p>
</div>
{/* Card */}
<div className="bg-zinc-900/80 backdrop-blur-2xl border border-white/10 rounded-2xl p-8 shadow-[0_25px_60px_-15px_rgba(0,0,0,0.5),0_0_40px_-10px_rgba(240,185,11,0.08)]">
<div className="bg-nofx-bg-lighter backdrop-blur-2xl border border-[rgba(26,24,19,0.14)] rounded-2xl p-8 shadow-lg">
<form onSubmit={handleSubmit} className="space-y-5">
{/* Email */}
<div>
<label className="block text-xs font-medium text-zinc-400 mb-2">{l.email}</label>
<label className="block text-xs font-medium text-nofx-text-muted mb-2">{l.email}</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-xl px-4 py-3 text-sm text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
placeholder={l.emailPlaceholder}
required
autoFocus
@@ -165,20 +165,20 @@ export function SetupPage() {
{/* Password */}
<div>
<label className="block text-xs font-medium text-zinc-400 mb-2">{l.password}</label>
<label className="block text-xs font-medium text-nofx-text-muted mb-2">{l.password}</label>
<div className="relative">
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-black/40 border border-white/10 rounded-xl px-4 py-3 pr-11 text-sm text-white placeholder-zinc-600 focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-xl px-4 py-3 pr-11 text-sm text-nofx-text placeholder-nofx-text-muted focus:outline-none focus:border-nofx-gold/60 focus:ring-1 focus:ring-nofx-gold/30 transition-all"
placeholder={l.passwordPlaceholder}
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-zinc-500 hover:text-zinc-300 transition-colors"
className="absolute right-3.5 top-1/2 -translate-y-1/2 text-nofx-text-muted hover:text-nofx-text transition-colors"
>
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
@@ -193,7 +193,7 @@ export function SetupPage() {
{/* Error */}
{error && (
<p className="text-xs text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-2">
<p className="text-xs text-nofx-danger bg-nofx-danger/10 border border-nofx-danger/20 rounded-lg px-3 py-2">
{error}
</p>
)}
@@ -202,14 +202,14 @@ export function SetupPage() {
<button
type="submit"
disabled={loading}
className="w-full bg-nofx-gold hover:bg-yellow-400 active:scale-[0.98] text-black font-semibold py-3 rounded-xl text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2 shadow-[0_0_20px_rgba(240,185,11,0.2)]"
className="w-full bg-nofx-gold hover:bg-nofx-gold-highlight active:scale-[0.98] text-nofx-bg font-semibold py-3 rounded-xl text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed mt-2"
>
{loading ? l.submitting : l.submit}
</button>
</form>
</div>
<p className="text-center text-xs text-zinc-600 mt-6">
<p className="text-center text-xs text-nofx-text-muted mt-6">
{l.singleUser}
</p>
</div>

View File

@@ -92,11 +92,11 @@ export function TwoStageKeyModal({
setProcessing(true)
try {
// 生成混淆字符串
// Generate obfuscation string
const obfuscation = generateObfuscation()
setManualObfuscationValue(obfuscation)
// 尝试复制到剪贴板
// Try to copy to clipboard
if (navigator.clipboard) {
try {
await navigator.clipboard.writeText(obfuscation)
@@ -105,14 +105,14 @@ export function TwoStageKeyModal({
...obfuscationLog,
`Stage 1: ${new Date().toISOString()} - Auto copied obfuscation`,
])
toast.success('已复制混淆字符串到剪贴板')
toast.success('Obfuscation string copied to clipboard')
} catch {
setClipboardStatus('failed')
setObfuscationLog([
...obfuscationLog,
`Stage 1: ${new Date().toISOString()} - Auto copy failed, manual required`,
])
toast.error('复制失败,请手动复制混淆字符串')
toast.error('Copy failed, please copy the obfuscation string manually')
}
} else {
setClipboardStatus('failed')
@@ -120,7 +120,7 @@ export function TwoStageKeyModal({
...obfuscationLog,
`Stage 1: ${new Date().toISOString()} - Clipboard API not available`,
])
toast('当前浏览器不支持自动复制,请手动复制')
toast('This browser does not support automatic copy, please copy manually')
}
setTimeout(() => {
@@ -179,17 +179,17 @@ export function TwoStageKeyModal({
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
<div className="bg-gray-900 p-8 rounded-xl max-w-lg w-full mx-4 border border-gray-700">
<div className="bg-nofx-bg-lighter p-8 rounded-xl max-w-lg w-full mx-4 border border-[rgba(26,24,19,0.14)]">
<div className="text-center mb-6">
<h2 className="text-xl font-bold text-white mb-2">
<h2 className="text-xl font-bold text-nofx-text mb-2">
🔐 {t('twoStageKey.title', language)}
{contextLabel && (
<span className="text-gray-300 text-base font-normal ml-2">
<span className="text-nofx-text-muted text-base font-normal ml-2">
({contextLabel})
</span>
)}
</h2>
<p className="text-gray-300 text-sm">
<p className="text-nofx-text-muted text-sm">
{stage === 1
? t('twoStageKey.stage1Description', language, {
length: expectedPart1Length,
@@ -208,7 +208,7 @@ export function TwoStageKeyModal({
{stage === 1 && (
<div className="space-y-4">
<div>
<label className="block text-gray-300 text-sm mb-2">
<label className="block text-nofx-text-muted text-sm mb-2">
{t('twoStageKey.stage1InputLabel', language)} (
{expectedPart1Length} {t('twoStageKey.characters', language)})
</label>
@@ -218,13 +218,13 @@ export function TwoStageKeyModal({
value={part1}
onChange={(e) => setPart1(e.target.value)}
placeholder="0x1234..."
className="w-full bg-gray-800 border border-gray-600 rounded-lg px-4 py-3 text-white font-mono text-sm focus:border-blue-500 focus:outline-none"
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-lg px-4 py-3 text-nofx-text font-mono text-sm focus:border-nofx-gold focus:outline-none"
maxLength={expectedPart1Length + 2} // +2 for optional 0x prefix
disabled={processing}
/>
</div>
{error && <div className="text-red-400 text-sm">{error}</div>}
{error && <div className="text-nofx-danger text-sm">{error}</div>}
<div className="flex gap-3">
<button
@@ -233,7 +233,7 @@ export function TwoStageKeyModal({
(part1.startsWith('0x') ? part1.slice(2) : part1).length <
expectedPart1Length || processing
}
className="flex-1 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white font-medium py-3 px-4 rounded-lg transition-colors"
className="flex-1 bg-nofx-gold hover:bg-nofx-gold-highlight disabled:bg-nofx-bg-deeper disabled:text-nofx-text-muted text-nofx-bg font-medium py-3 px-4 rounded-lg transition-colors"
>
{processing
? t('twoStageKey.processing', language)
@@ -242,7 +242,7 @@ export function TwoStageKeyModal({
<button
onClick={onCancel}
disabled={processing}
className="px-6 py-3 text-gray-300 hover:text-white border border-gray-600 rounded-lg transition-colors"
className="px-6 py-3 text-nofx-text-muted hover:text-nofx-text border border-[rgba(26,24,19,0.14)] rounded-lg transition-colors"
>
{t('twoStageKey.cancelButton', language)}
</button>
@@ -252,9 +252,9 @@ export function TwoStageKeyModal({
{/* Transition Message */}
{stage === 2 && clipboardStatus !== 'idle' && (
<div className="mb-4 p-4 rounded-lg bg-blue-900/50 border border-blue-600">
<div className="mb-4 p-4 rounded-lg bg-nofx-gold/10 border border-nofx-gold/40">
{clipboardStatus === 'copied' && (
<div className="text-blue-300">
<div className="text-nofx-text">
<div className="font-medium">
{t('twoStageKey.obfuscationCopied', language)}
</div>
@@ -264,11 +264,11 @@ export function TwoStageKeyModal({
</div>
)}
{clipboardStatus === 'failed' && manualObfuscationValue && (
<div className="text-yellow-300">
<div className="text-nofx-gold">
<div className="font-medium">
{t('twoStageKey.obfuscationManual', language)}
</div>
<div className="text-xs mt-2 p-2 bg-gray-800 rounded font-mono break-all border">
<div className="text-xs mt-2 p-2 bg-nofx-bg-deeper rounded font-mono break-all border border-[rgba(26,24,19,0.14)]">
{manualObfuscationValue}
</div>
<div className="text-sm mt-1">
@@ -283,7 +283,7 @@ export function TwoStageKeyModal({
{stage === 2 && (
<div className="space-y-4">
<div>
<label className="block text-gray-300 text-sm mb-2">
<label className="block text-nofx-text-muted text-sm mb-2">
{t('twoStageKey.stage2InputLabel', language)} (
{expectedPart2Length} {t('twoStageKey.characters', language)})
</label>
@@ -293,12 +293,12 @@ export function TwoStageKeyModal({
value={part2}
onChange={(e) => setPart2(e.target.value)}
placeholder="...5678"
className="w-full bg-gray-800 border border-gray-600 rounded-lg px-4 py-3 text-white font-mono text-sm focus:border-blue-500 focus:outline-none"
className="w-full bg-nofx-bg-deeper border border-[rgba(26,24,19,0.14)] rounded-lg px-4 py-3 text-nofx-text font-mono text-sm focus:border-nofx-gold focus:outline-none"
maxLength={expectedPart2Length + 2}
/>
</div>
{error && <div className="text-red-400 text-sm">{error}</div>}
{error && <div className="text-nofx-danger text-sm">{error}</div>}
<div className="flex gap-3">
<button
@@ -307,13 +307,13 @@ export function TwoStageKeyModal({
(part2.startsWith('0x') ? part2.slice(2) : part2).length <
expectedPart2Length
}
className="flex-1 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 text-white font-medium py-3 px-4 rounded-lg transition-colors"
className="flex-1 bg-nofx-success hover:bg-nofx-success/90 disabled:bg-nofx-bg-deeper disabled:text-nofx-text-muted text-nofx-bg font-medium py-3 px-4 rounded-lg transition-colors"
>
🔒 {t('twoStageKey.encryptButton', language)}
</button>
<button
onClick={handleReset}
className="px-6 py-3 text-gray-300 hover:text-white border border-gray-600 rounded-lg transition-colors"
className="px-6 py-3 text-nofx-text-muted hover:text-nofx-text border border-[rgba(26,24,19,0.14)] rounded-lg transition-colors"
>
{t('twoStageKey.backButton', language)}
</button>