From aa789d739c2d7e1a80c39fe05e4087a5915080fb Mon Sep 17 00:00:00 2001 From: Thigazhezhilan J Date: Sun, 22 Mar 2026 14:37:23 +0530 Subject: [PATCH] thigal_test --- src/api/strategy.js | 18 ++--- src/components/StrategyTimeline.tsx | 5 +- src/components/landing/HeroSection.tsx | 2 +- src/lib/queryClient.ts | 29 ++++++-- src/pages/PaperPortfolio.tsx | 98 ++++++++++++++++++-------- vite.config.ts | 5 +- 6 files changed, 103 insertions(+), 54 deletions(-) diff --git a/src/api/strategy.js b/src/api/strategy.js index 3be7b547..afd2236e 100644 --- a/src/api/strategy.js +++ b/src/api/strategy.js @@ -1,26 +1,16 @@ -const API_BASE = "/api"; +import { apiRequest } from "@/lib/queryClient"; export async function startStrategy(data) { - const res = await fetch(`${API_BASE}/strategy/start`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - credentials: "include", - body: JSON.stringify(data), - }); + const res = await apiRequest("POST", "/strategy/start", data); return res.json(); } export async function stopStrategy() { - const res = await fetch(`${API_BASE}/strategy/stop`, { - method: "POST", - credentials: "include", - }); + const res = await apiRequest("POST", "/strategy/stop"); return res.json(); } export async function getStrategyStatus() { - const res = await fetch(`${API_BASE}/strategy/status`, { - credentials: "include", - }); + const res = await apiRequest("GET", "/strategy/status"); return res.json(); } diff --git a/src/components/StrategyTimeline.tsx b/src/components/StrategyTimeline.tsx index 499233d5..a39074d7 100644 --- a/src/components/StrategyTimeline.tsx +++ b/src/components/StrategyTimeline.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useRef, useState } from "react"; +import { apiRequest } from "@/lib/queryClient"; type StrategyEvent = { seq?: number; @@ -110,9 +111,7 @@ export default function StrategyTimeline() { useEffect(() => { const fetchLogs = async () => { try { - const res = await fetch(`/api/logs?since_seq=${latestSeqRef.current}`, { - credentials: "include", - }); + const res = await apiRequest("GET", `/logs?since_seq=${latestSeqRef.current}`); const data = await res.json(); const events = Array.isArray(data?.events) ? data.events : []; const normalized = events.map(normalizeLog); diff --git a/src/components/landing/HeroSection.tsx b/src/components/landing/HeroSection.tsx index 9a904f23..f5bf63d4 100644 --- a/src/components/landing/HeroSection.tsx +++ b/src/components/landing/HeroSection.tsx @@ -250,7 +250,7 @@ export default function HeroSection({ onExploreStrategies }: HeroSectionProps) { data-testid="button-learn-more" onClick={() => navigate("/learn-more")} > - Learn More + Learn More Test diff --git a/src/lib/queryClient.ts b/src/lib/queryClient.ts index 9af1f104..1c956956 100644 --- a/src/lib/queryClient.ts +++ b/src/lib/queryClient.ts @@ -5,11 +5,12 @@ const API_BASE_URL = ((typeof window !== "undefined" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1")) as any - ? "http://localhost:8000" + ? "http://localhost:8000/api" : undefined); const NORMALIZED_API_BASE_URL = API_BASE_URL ? API_BASE_URL.replace(/\/+$/, "") : ""; +const REQUEST_TIMEOUT_MS = 12000; function resolveApiUrl(url: string) { if (url.startsWith("http://") || url.startsWith("https://")) { @@ -18,7 +19,8 @@ function resolveApiUrl(url: string) { if (!NORMALIZED_API_BASE_URL) { return url; } - return `${NORMALIZED_API_BASE_URL}${url}`; + const normalizedPath = url.startsWith("/") ? url : `/${url}`; + return `${NORMALIZED_API_BASE_URL}${normalizedPath}`; } async function throwIfResNotOk(res: Response) { @@ -28,12 +30,31 @@ async function throwIfResNotOk(res: Response) { } } +async function fetchWithTimeout(input: RequestInfo | URL, init?: RequestInit) { + const controller = new AbortController(); + const timeoutId = globalThis.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + + try { + return await fetch(input, { + ...init, + signal: controller.signal, + }); + } catch (error) { + if (error instanceof DOMException && error.name === "AbortError") { + throw new Error("Request timed out. Please try again."); + } + throw error; + } finally { + globalThis.clearTimeout(timeoutId); + } +} + export async function apiRequest( method: string, url: string, data?: unknown | undefined, ): Promise { - const res = await fetch(resolveApiUrl(url), { + const res = await fetchWithTimeout(resolveApiUrl(url), { method, headers: data ? { "Content-Type": "application/json" } : {}, body: data ? JSON.stringify(data) : undefined, @@ -50,7 +71,7 @@ export const getQueryFn: (options: { }) => QueryFunction = ({ on401: unauthorizedBehavior }) => async ({ queryKey }) => { - const res = await fetch(resolveApiUrl(queryKey.join("/") as string), { + const res = await fetchWithTimeout(resolveApiUrl(queryKey.join("/") as string), { credentials: "include", }); diff --git a/src/pages/PaperPortfolio.tsx b/src/pages/PaperPortfolio.tsx index 7ed51553..c7df8f84 100644 --- a/src/pages/PaperPortfolio.tsx +++ b/src/pages/PaperPortfolio.tsx @@ -246,12 +246,7 @@ function PaperTradingPortfolio() { let timer: number; const poll = async () => { try { - const res = await fetch("/paper/mtm", { - credentials: "include", - }); - if (!res.ok) { - return; - } + const res = await apiRequest("GET", "/paper/mtm"); const data: PaperMtmResponse = await res.json(); setMtmPositions(Array.isArray(data.positions) ? data.positions : []); if (typeof data.equity === "number") { @@ -307,12 +302,7 @@ function PaperTradingPortfolio() { useEffect(() => { const fetchMarketStatus = async () => { try { - const res = await fetch("/market/status", { - credentials: "include", - }); - if (!res.ok) { - return; - } + const res = await apiRequest("GET", "/market/status"); const data: MarketStatusResponse = await res.json(); setMarketStatus(data); } catch { @@ -340,10 +330,7 @@ function PaperTradingPortfolio() { setIsResetting(true); try { - const res = await fetch("/paper/reset", { method: "POST" }); - if (!res.ok) { - throw new Error("Reset failed"); - } + await apiRequest("POST", "/paper/reset"); setMtmPositions([]); setMtmEquity(null); setMtmInitialCash(null); @@ -353,16 +340,21 @@ function PaperTradingPortfolio() { setMtmPnlPoints([]); skipFirstPnlPointRef.current = true; setInitialCash(""); - fundsQuery.refetch(); - positionsQuery.refetch(); - ordersQuery.refetch(); - await refreshStatus(); + await Promise.all([ + fundsQuery.refetch(), + positionsQuery.refetch(), + ordersQuery.refetch(), + refreshStatus(), + ]); setTimelineKey((value) => value + 1); - setIsResetting(false); } catch (error) { - setIsResetting(false); - alert("Reset failed. Check backend logs."); + toast({ + title: "Reset failed", + description: error instanceof Error ? error.message : "Please try again.", + }); console.error(error); + } finally { + setIsResetting(false); } }; @@ -395,9 +387,26 @@ function PaperTradingPortfolio() { description: "Paper trading is the only available mode right now.", }); } + if (result?.status === "started" || result?.status === "restarted") { + toast({ + title: "Paper strategy started", + description: "The simulator is now running.", + }); + } + } catch (error) { + toast({ + title: "Start failed", + description: error instanceof Error ? error.message : "Please try again.", + }); + console.error(error); } finally { setIsStarting(false); - await refreshStatus(); + await Promise.all([ + refreshStatus(), + fundsQuery.refetch(), + positionsQuery.refetch(), + ordersQuery.refetch(), + ]); } }; @@ -405,9 +414,24 @@ function PaperTradingPortfolio() { setIsStopping(true); try { await stopStrategy(); + toast({ + title: "Paper strategy stopped", + description: "The simulator has been stopped.", + }); + } catch (error) { + toast({ + title: "Stop failed", + description: error instanceof Error ? error.message : "Please try again.", + }); + console.error(error); } finally { setIsStopping(false); - await refreshStatus(); + await Promise.all([ + refreshStatus(), + fundsQuery.refetch(), + positionsQuery.refetch(), + ordersQuery.refetch(), + ]); } }; @@ -418,8 +442,12 @@ function PaperTradingPortfolio() { ? "WAITING" : "STOPPED"; const canStart = typeof initialCash === "number" && initialCash >= 10000; + const canStop = + normalizedStrategyStatus === "RUNNING" || + normalizedStrategyStatus === "WAITING"; + const strategyLocked = canStop || isStarting || isStopping || isResetting; const canAddCash = - addCashEnabled && typeof addCashAmount === "number" && addCashAmount > 0; + canStop && addCashEnabled && typeof addCashAmount === "number" && addCashAmount > 0; const strategyBadgeClass = normalizedStrategyStatus === "RUNNING" ? "bg-green-500 text-white" @@ -568,7 +596,7 @@ function PaperTradingPortfolio() { type="number" min={10000} value={initialCash} - disabled={normalizedStrategyStatus === "RUNNING"} + disabled={strategyLocked} onChange={(event) => { const raw = event.target.value; if (raw === "") { @@ -588,6 +616,7 @@ function PaperTradingPortfolio() { setAddCashEnabled(value === true)} /> @@ -601,7 +630,7 @@ function PaperTradingPortfolio() { min={1} step={100} value={addCashAmount} - disabled={!addCashEnabled || isAddingCash} + disabled={!canStop || !addCashEnabled || isAddingCash || isStarting || isStopping || isResetting} onChange={(event) => { const raw = event.target.value; if (raw === "") { @@ -634,6 +663,7 @@ function PaperTradingPortfolio() { min={0} step={100} value={sipAmount} + disabled={strategyLocked} onChange={(event) => { const value = Number(event.target.value); setSipAmount(Number.isNaN(value) ? 0 : value); @@ -650,6 +680,7 @@ function PaperTradingPortfolio() { min={1} step={1} value={frequencyValue} + disabled={strategyLocked} onChange={(event) => { const value = Number(event.target.value); setFrequencyValue(Number.isNaN(value) ? 1 : value); @@ -661,6 +692,7 @@ function PaperTradingPortfolio() {