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:
+3
-1
@@ -37,7 +37,9 @@ export async function getPost(id: number): Promise<TrumpPost> {
|
||||
return fetchJson<TrumpPost>(`/posts/${id}`)
|
||||
}
|
||||
|
||||
export async function getPrices(asset: 'BTC' | 'ETH', tf: string): Promise<Candle[]> {
|
||||
// asset widened from 'BTC' | 'ETH' to string — chart will eventually support
|
||||
// SOL/TRUMP etc. once the backend /prices/{asset} endpoint covers them.
|
||||
export async function getPrices(asset: string, tf: string): Promise<Candle[]> {
|
||||
return fetchJson<Candle[]>(`/prices/${asset}?tf=${tf}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import { useWsSubscribe } from './wsContext'
|
||||
|
||||
interface Handlers {
|
||||
onPrice?: (asset: 'BTC' | 'ETH', price: number) => void
|
||||
// BUG-08 fix: backend now broadcasts SOL/TRUMP/BNB/DOGE/LINK/AAVE ticks
|
||||
// in addition to BTC/ETH — widen asset to string.
|
||||
onPrice?: (asset: string, price: number) => void
|
||||
onNewPost?: (post: object) => void
|
||||
}
|
||||
|
||||
type PriceMsg = { type: 'price'; asset: 'BTC' | 'ETH'; price: number }
|
||||
type PriceMsg = { type: 'price'; asset: string; price: number }
|
||||
type PostMsg = { type: 'new_post'; post: object }
|
||||
|
||||
/**
|
||||
|
||||
+35
-5
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user