gui-ts: prevent stale run overwrites and expose model mismatch

Guard async solve updates with run tokens and surface FDM/FEA card length mismatch in Results so users understand when overlays are intentionally reduced.

Made-with: Cursor
This commit is contained in:
2026-04-17 08:23:30 -06:00
parent 01710af161
commit 6a4380bf42
2 changed files with 23 additions and 1 deletions

View File

@@ -48,6 +48,7 @@ export function App() {
const [surfaceCardQaError, setSurfaceCardQaError] = useState<string | null>(null);
const [validatingSurfaceCard, setValidatingSurfaceCard] = useState(false);
const hydrated = useRef(false);
const latestRunToken = useRef(0);
const engineeringChecks = useMemo(() => runEngineeringChecks(store.state), [store.state]);
useEffect(() => {
@@ -96,6 +97,8 @@ export function App() {
}, [parsedSurfaceCard]);
const handleRun = useCallback(async () => {
const runToken = latestRunToken.current + 1;
latestRunToken.current = runToken;
if (engineeringChecks.hasBlockingError) {
setError("Please fix blocking engineering checks before running the solver.");
setStatusMessage("Blocked by engineering checks");
@@ -133,6 +136,9 @@ export function App() {
fourierHarmonics: runSettings.outputOptions.fourierHarmonics
}
});
if (latestRunToken.current !== runToken) {
return;
}
setResult(resp);
const dt = (performance.now() - t0) / 1000;
setElapsed(dt);
@@ -140,11 +146,16 @@ export function App() {
setStatusMessage(`Done in ${dt.toFixed(1)}s`);
setActiveTab("tab-results");
} catch (e) {
if (latestRunToken.current !== runToken) {
return;
}
setError(e instanceof Error ? e.message : String(e));
setStatusMessage("Error");
} finally {
if (latestRunToken.current === runToken) {
setLoading(false);
}
}
}, [engineeringChecks.hasBlockingError, parsedSurfaceCard, runSettings, store.state]);
const handleExportXml = useCallback(() => {

View File

@@ -122,6 +122,12 @@ export function ResultsTab({
}
return [fdmSeries.position, fdmSeries.polished, fdmSeries.downhole] as AlignedData;
}, [fdm, fea]);
const hasCardLengthMismatch = useMemo(() => {
const fdmSeries = toSeries(fdm ?? undefined);
const feaSeries = toSeries(fea ?? undefined);
if (!fdmSeries || !feaSeries) return false;
return fdmSeries.position.length !== feaSeries.position.length;
}, [fdm, fea]);
const dynacardOptions = useMemo<Options>(() => {
const seriesCount = fea ? 5 : 3;
@@ -364,6 +370,11 @@ export function ResultsTab({
</Fieldset>
<Fieldset legend="Dynamometer Card">
{hasCardLengthMismatch && (
<div className="callout">
FDM and FEA card lengths differ; chart overlay is limited to FDM series for this run.
</div>
)}
{dynacardData ? (
<UPlotChart data={dynacardData} options={dynacardOptions} height={340} />
) : (