fix: pre-launch UI hardening + KOL reduce-action type, proxy IP relay, settings redesign

Frontend half of the pre-launch audit campaign:

- types/index.ts + kol/KolPageClient.tsx: add missing 'reduce' KolAction
  (backend emits it; frontend lacked the type + color/label maps → undefined
  styling). Adds ACTION_COLOR/actionLabel/postActionLabel entries.
- proxy/[...path]/route.ts: relay real client IP (x-forwarded-for / x-real-ip)
  so the backend rate limiter buckets per-user instead of per-Next-server (BUG-02).
- Settings/BotConfigPanel redesign, paper-mode clarity, copy cleanup.
- Assorted page/display fixes, loading states, Pagination component.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
k
2026-05-29 11:57:43 +08:00
parent b76de36af0
commit d50c05b120
32 changed files with 1003 additions and 224 deletions
+38 -7
View File
@@ -179,11 +179,20 @@ export default function OpenPositions() {
useEffect(() => { setMounted(true) }, [])
const [closeErr, setCloseErr] = useState('')
const [growId, setGrowId] = useState<number | null>(null)
// Separate error slot for the Grow toggle so a transient toggle failure
// doesn't squat in the panel-header `err` banner (which is otherwise
// owned by the 15s poll). Auto-clears after 4s.
const [growErr, setGrowErr] = useState('')
useEffect(() => {
if (!growErr) return
const t = setTimeout(() => setGrowErr(''), 4000)
return () => clearTimeout(t)
}, [growErr])
async function toggleGrow(p: OpenPosition) {
if (!address || growId !== null) return
const next = !p.grow_mode
setGrowId(p.trade_id)
setGrowId(p.trade_id); setGrowErr('')
try {
const env = await signRequest({
action: 'set_trade_grow', wallet: address,
@@ -194,9 +203,9 @@ export default function OpenPositions() {
x.trade_id === p.trade_id ? { ...x, grow_mode: r.grow_mode } : x))
} catch (e) {
if (isUserRejection(e)) {
setErr(isZh ? 'Grow 切换已取消' : 'Grow toggle cancelled')
setGrowErr(isZh ? 'Grow 切换已取消' : 'Grow toggle cancelled')
} else {
setErr(walletErrorLabel(e, isZh ? 'Grow 切换已取消' : 'Grow toggle cancelled', 90)
setGrowErr(walletErrorLabel(e, isZh ? 'Grow 切换已取消' : 'Grow toggle cancelled', 90)
|| (isZh ? 'Grow 切换失败' : 'grow toggle failed'))
}
} finally { setGrowId(null) }
@@ -237,7 +246,12 @@ export default function OpenPositions() {
void load()
const id = setInterval(() => { void load() }, POLL_MS)
return () => { cancelled = true; clearInterval(id) }
}, [address, isConnected, signMessageAsync, isZh])
// signMessageAsync and isZh are intentionally excluded: signMessageAsync is
// never called inside this effect (load() uses cached envelopes only), and
// isZh is a compile-time constant (always false). Including either would
// restart the 15-second poll timer on every wagmi reference change.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [address, isConnected])
// Trigger close. Two-step: opens modal, user confirms, we sign + POST.
async function confirmCloseTrade() {
@@ -257,8 +271,15 @@ export default function OpenPositions() {
setClosing(null)
setCloseState('idle')
} catch (e: unknown) {
setCloseErr(walletErrorLabel(e, isZh ? '已取消' : 'Cancelled', 120))
setCloseState('err')
// Distinguish a user-cancelled signature (benign, close the modal) from
// a real failure (keep modal open in 'err' state so user can retry or
// read the error).
if (isUserRejection(e)) {
setClosing(null); setCloseState('idle'); setCloseErr('')
} else {
setCloseErr(walletErrorLabel(e, isZh ? '已取消' : 'Cancelled', 120))
setCloseState('err')
}
}
}
@@ -351,6 +372,9 @@ export default function OpenPositions() {
{err && (
<span style={{ fontSize: 11, color: 'var(--down)' }}> {err}</span>
)}
{growErr && !err && (
<span style={{ fontSize: 11, color: 'var(--down)' }}> {growErr}</span>
)}
</div>
{/* Position list (or empty state) */}
@@ -396,7 +420,14 @@ export default function OpenPositions() {
presses "Close now", reads the impact summary, then confirms. */}
{closing && (
<div
onClick={() => closeState === 'idle' && setClosing(null)}
onClick={() => {
// Backdrop dismisses in both idle (never started) and err
// (failed, user wants out) states. NOT during signing/closing —
// the wallet popup or in-flight POST shouldn't be orphaned.
if (closeState === 'idle' || closeState === 'err') {
setClosing(null); setCloseState('idle'); setCloseErr('')
}
}}
style={{
position: 'fixed', inset: 0, zIndex: 1000,
background: 'rgba(0,0,0,0.55)',