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:
-
@@ -578,7 +639,7 @@ export default function StockDetail() {
{/* Latest analysis result */}
- {displayJob && }
+ {displayJob && }
{/* Expand toggle */}
{displayJob && (