diff --git a/app/routes/analyze.ticker.tsx b/app/routes/analyze.ticker.tsx index cf2a121..e46e885 100644 --- a/app/routes/analyze.ticker.tsx +++ b/app/routes/analyze.ticker.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from "react"; -import { useLoaderData, useNavigate, useLocation, Link } from "react-router"; +import { useLoaderData, Link, useSearchParams } from "react-router"; import TradingViewChart from "../components/TradingViewChart"; import Navbar from "../components/Navbar"; import { useMemo } from "react"; @@ -324,8 +324,27 @@ function JobHistoryInline({ ticker, runningJob, latestJob, onJobSelect }: { tick ); } -function TradingResultCard({ job, expanded }: { job: any; expanded: boolean }) { +function TradingResultCard({ job, expanded, onRefresh }: { job: any; expanded: boolean; onRefresh?: () => void }) { const decision = job?.returnValue; + const isRunning = job && !decision && (job.state === "active" || job.state === "waiting" || job.state === "queued" || job.state === "processing"); + + if (isRunning) { + return ( +
+
+

Job in Progress

+ {onRefresh && } +
+
+ + {job.id} + {stateBadge(job.state)} +
+

The TradingGraph analysis is running. Results will appear here when complete.

+
+ ); + } + if (!decision) return null; return ( @@ -367,8 +386,7 @@ function TradingResultCard({ job, expanded }: { job: any; expanded: boolean }) { export default function StockDetail() { const { ticker, position, orders, bars, timeframe, range, stockRecord, latestJob, runningJob } = useLoaderData() as LoaderData; - const navigate = useNavigate(); - const location = useLocation(); + const [searchParams, setSearchParams] = useSearchParams(); const [analysisLoading, setAnalysisLoading] = useState(false); const [jobStatus, setJobStatus] = useState(runningJob || null); @@ -376,6 +394,40 @@ export default function StockDetail() { const [showExpanded, setShowExpanded] = useState(false); const [selectedJob, setSelectedJob] = useState(latestJob || null); + // Poll selected job if it's in progress + useEffect(() => { + if (!selectedJob?.id) return; + const state = selectedJob.state; + if (state !== "active" && state !== "waiting" && state !== "queued" && state !== "processing") return; + + let cancelled = false; + let timer: ReturnType | null = null; + let currentController: AbortController | null = null; + + const poll = async () => { + if (currentController) { try { currentController.abort(); } catch (e) {} } + currentController = new AbortController(); + try { + const res = await fetch(`/api/jobs/${selectedJob.id}`, { signal: currentController.signal }); + if (!res.ok) { if (!cancelled) timer = setTimeout(poll, 2000); return; } + const j = await res.json(); + if (cancelled) return; + setSelectedJob(j); + if (j.state === "completed" || j.state === "failed") { + cancelled = true; + // Also update the main job status if this is the same job + if (jobStatus?.id === j.id) setJobStatus(j); + } + } catch (e: any) { + if (e?.name !== "AbortError") console.warn("Selected job poll error:", e); + } finally { + if (!cancelled) timer = setTimeout(poll, 2000); + } + }; + poll(); + return () => { cancelled = true; if (timer) clearTimeout(timer); if (currentController) { try { currentController.abort(); } catch (e) {} } }; + }, [selectedJob?.id, selectedJob?.state, jobStatus?.id]); + const cacheKey = `tradinggraph-${ticker}`; // Poll running job if exists @@ -446,10 +498,7 @@ export default function StockDetail() { }, [ticker]); const updateParams = (newTimeframe: string, newRange: string) => { - const searchParams = new URLSearchParams(location.search); - searchParams.set("timeframe", newTimeframe); - searchParams.set("range", newRange); - navigate(`${location.pathname}?${searchParams.toString()}`, { replace: true }); + setSearchParams({ timeframe: newTimeframe, range: newRange }, { replace: true, preventScrollReset: true }); }; const runTradingGraph = async () => { @@ -538,6 +587,18 @@ export default function StockDetail() { bar.time && bar.open != null && index === arr.findIndex((b: any) => b.time === bar.time) ) || []; + const refreshSelectedJob = useCallback(async () => { + if (!selectedJob?.id) return; + try { + const res = await fetch(`/api/jobs/${selectedJob.id}`); + if (res.ok) { + const j = await res.json(); + setSelectedJob(j); + if (jobStatus?.id === j.id) setJobStatus(j); + } + } catch (e) { console.warn("Refresh job error:", e); } + }, [selectedJob?.id, jobStatus?.id]); + const displayJob = selectedJob || (jobStatus?.state === "completed" ? jobStatus : null); return ( @@ -560,11 +621,11 @@ export default function StockDetail() {
Timeframe: - updateParams(e.target.value, range)} className="border border-gray-300 rounded px-2 py-1 text-sm text-gray-900 bg-white focus:ring-2 focus:ring-blue-500"> {TIMEFRAMES.map((tf) => )} Range: - updateParams(timeframe, e.target.value)} className="border border-gray-300 rounded px-2 py-1 text-sm text-gray-900 bg-white focus:ring-2 focus:ring-blue-500"> {RANGES.map((r) => )}
@@ -578,7 +639,7 @@ export default function StockDetail() {
{/* Latest analysis result */} - {displayJob && } + {displayJob && } {/* Expand toggle */} {displayJob && (