fix: show running job status when viewing in-progress jobs and auto-poll for completion
This commit is contained in:
@@ -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 (
|
||||
<div className="bg-white rounded-xl shadow-lg p-4 border border-gray-200">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-gray-800">Job in Progress</h3>
|
||||
{onRefresh && <button onClick={onRefresh} className="text-xs text-blue-600 hover:underline">Refresh</button>}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="animate-pulse w-2.5 h-2.5 bg-blue-500 rounded-full" />
|
||||
<span className="font-mono text-xs text-gray-600 truncate">{job.id}</span>
|
||||
{stateBadge(job.state)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">The TradingGraph analysis is running. Results will appear here when complete.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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<any>(runningJob || null);
|
||||
@@ -376,6 +394,40 @@ export default function StockDetail() {
|
||||
const [showExpanded, setShowExpanded] = useState(false);
|
||||
const [selectedJob, setSelectedJob] = useState<any>(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<typeof setTimeout> | 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() {
|
||||
<div className="bg-white rounded-xl shadow-lg p-4 mb-4 border border-gray-200">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className="text-xs text-gray-500">Timeframe:</span>
|
||||
<select value={timeframe} onChange={(e) => updateParams(e.target.value, range)} className="border border-gray-300 rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500">
|
||||
<select value={timeframe} onChange={(e) => 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) => <option key={tf.value} value={tf.value}>{tf.label}</option>)}
|
||||
</select>
|
||||
<span className="text-xs text-gray-500">Range:</span>
|
||||
<select value={range} onChange={(e) => updateParams(timeframe, e.target.value)} className="border border-gray-300 rounded px-2 py-1 text-sm focus:ring-2 focus:ring-blue-500">
|
||||
<select value={range} onChange={(e) => 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) => <option key={r.value} value={r.value}>{r.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
@@ -578,7 +639,7 @@ export default function StockDetail() {
|
||||
</div>
|
||||
|
||||
{/* Latest analysis result */}
|
||||
{displayJob && <TradingResultCard job={displayJob} expanded={showExpanded} />}
|
||||
{displayJob && <TradingResultCard job={displayJob} expanded={showExpanded} onRefresh={refreshSelectedJob} />}
|
||||
|
||||
{/* Expand toggle */}
|
||||
{displayJob && (
|
||||
|
||||
Reference in New Issue
Block a user