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
+35 -5
View File
@@ -29,6 +29,15 @@ const WsContext = createContext<WsContextValue | null>(null)
const WS_URL = process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8000'
// Exponential backoff: 2s → 4s → 8s → … capped at 60s, plus ±30% jitter
// so a server restart doesn't get hit by all clients at the same instant.
const WS_RETRY_BASE_MS = 2_000
const WS_RETRY_MAX_MS = 60_000
function retryDelay(attempt: number): number {
const exp = Math.min(WS_RETRY_BASE_MS * 2 ** attempt, WS_RETRY_MAX_MS)
return exp * (0.7 + Math.random() * 0.6) // ±30% jitter
}
export function WsProvider({ children }: { children: ReactNode }) {
// Stable map: message-type → set of handlers. Never replaced, only mutated.
const subs = useRef<Map<string, Set<Handler>>>(new Map())
@@ -36,10 +45,7 @@ export function WsProvider({ children }: { children: ReactNode }) {
useEffect(() => {
let dead = false
let retryTimer: ReturnType<typeof setTimeout> | null = null
// Hold the current socket at useEffect scope so cleanup can close it.
// Previously `ws` lived inside connect() and cleanup couldn't reach it,
// leaking one connection per StrictMode remount and one per [locale]
// layout swap (which remounts WsProvider).
let attempt = 0
let socket: WebSocket | null = null
function connect() {
@@ -47,6 +53,10 @@ export function WsProvider({ children }: { children: ReactNode }) {
socket = new WebSocket(`${WS_URL}/ws/prices`)
const ws = socket // local alias for handler closures
ws.onopen = () => {
attempt = 0 // reset backoff on successful connection
}
ws.onmessage = (e) => {
try {
const msg = JSON.parse(e.data) as { type?: string }
@@ -58,14 +68,34 @@ export function WsProvider({ children }: { children: ReactNode }) {
}
ws.onclose = () => {
if (!dead) retryTimer = setTimeout(connect, 3000)
if (!dead) {
const delay = retryDelay(attempt++)
retryTimer = setTimeout(connect, delay)
}
}
ws.onerror = () => ws.close()
}
// Reconnect immediately when the tab comes back to the foreground
// (the socket may have silently died while the tab was hidden).
function onVisible() {
if (document.visibilityState !== 'visible') return
if (socket && socket.readyState === WebSocket.OPEN) return
// Cancel any pending retry — we're reconnecting now.
if (retryTimer) { clearTimeout(retryTimer); retryTimer = null }
attempt = 0
if (socket && socket.readyState < WebSocket.CLOSING) {
socket.onclose = null
socket.close()
}
connect()
}
document.addEventListener('visibilitychange', onVisible)
connect()
return () => {
dead = true
document.removeEventListener('visibilitychange', onVisible)
if (retryTimer) clearTimeout(retryTimer)
// Actively close any live socket so the OS-level connection releases
// immediately on unmount instead of waiting for the next server keepalive.