fix(simple): harden form runtime behavior

This commit is contained in:
root 2026-05-06 07:13:37 +00:00
parent f57f0fc205
commit 943cfda6b5

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import type { FormEvent } from 'react';
import { Pencil, RefreshCw, Trash2 } from 'lucide-react';
import { useAppContext } from '../context/AppContext';
@ -323,6 +323,22 @@ function normalizeKnownSymbol(value: unknown): string | null {
return normalized ? normalized : null;
}
function normalizeRuntimeArray<T>(value: unknown): T[] {
if (Array.isArray(value)) return value as T[];
if (value && typeof value === 'object' && Symbol.iterator in (value as object)) {
try {
return Array.from(value as Iterable<T>);
} catch {
return [];
}
}
return [];
}
function isLikelySymbolCandidate(symbol: string): boolean {
return /^[A-Z0-9./-]{1,20}$/.test(symbol);
}
function describeSavedSetup(entry: ManualEntryPayload): string {
const side = normalizeSetupSide(entry.simple_side);
const symbol = String(entry.symbol || '').trim().toUpperCase();
@ -367,17 +383,20 @@ export function SimpleView() {
const [loadingPrice, setLoadingPrice] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const marketPriceRequestSymbolRef = useRef<string>('');
const normalizedSymbol = draft.symbol.trim().toUpperCase();
const livePrice = normalizedSymbol ? Number(botState.symbols?.[normalizedSymbol]?.price || 0) : 0;
const symbolState = botState?.symbols && typeof botState.symbols === 'object' ? botState.symbols : {};
const livePrice = normalizedSymbol ? Number(symbolState?.[normalizedSymbol]?.price || 0) : 0;
const simpleAutoProfile = useMemo(
() => profiles.find((profile) => matchesSimpleAutoProfile(profile)) || null,
[profiles],
);
const simpleHoldings = useMemo(() => {
const positions = normalizeRuntimeArray<typeof botState.positions[number]>(botState?.positions);
const simpleProfileId = simpleAutoProfile?.id;
return botState.positions
return positions
.filter((position) => !simpleProfileId || position.profileId === simpleProfileId)
.map((position) => ({
symbol: String(position.symbol || '').trim().toUpperCase(),
@ -387,14 +406,15 @@ export function SimpleView() {
tradeId: position.tradeId,
}))
.filter((position) => position.symbol && position.size > 0 && position.entryPrice > 0);
}, [botState.positions, simpleAutoProfile?.id]);
}, [botState?.positions, simpleAutoProfile?.id]);
const runtimeOrders = useMemo(() => {
return Array.from(botState.orders.values()).filter((order) => {
const orders = normalizeRuntimeArray<Record<string, any>>(botState?.orders);
return orders.filter((order) => {
if (!simpleAutoProfile?.id) return true;
return String(order.profileId || '').trim() === simpleAutoProfile.id;
});
}, [botState.orders, simpleAutoProfile?.id]);
}, [botState?.orders, simpleAutoProfile?.id]);
const matchingHolding = useMemo(
() => simpleHoldings.find((holding) => holding.symbol === normalizedSymbol) || null,
@ -402,11 +422,11 @@ export function SimpleView() {
);
const supportedSymbols = useMemo(() => {
const fromState = Object.keys(botState.symbols || {}).map(normalizeKnownSymbol).filter(Boolean) as string[];
const fromState = Object.keys(symbolState || {}).map(normalizeKnownSymbol).filter(Boolean) as string[];
const fromSetups = savedSetups.map((entry) => normalizeKnownSymbol(entry.symbol)).filter(Boolean) as string[];
const fromHoldings = simpleHoldings.map((holding) => normalizeKnownSymbol(holding.symbol)).filter(Boolean) as string[];
return Array.from(new Set([...fromState, ...fromSetups, ...fromHoldings, ...COMMON_SIMPLE_SYMBOLS])).sort((left, right) => left.localeCompare(right));
}, [botState.symbols, savedSetups, simpleHoldings]);
}, [symbolState, savedSetups, simpleHoldings]);
const filteredSymbolSuggestions = useMemo(() => {
if (!normalizedSymbol) {
@ -457,8 +477,22 @@ export function SimpleView() {
return;
}
void handleLoadMarketPrice();
}, [normalizedSymbol, livePrice]);
if (!isLikelySymbolCandidate(normalizedSymbol)) {
return;
}
if (!supportedSymbols.includes(normalizedSymbol)) {
return;
}
const timer = window.setTimeout(() => {
void handleLoadMarketPrice();
}, 250);
return () => {
window.clearTimeout(timer);
};
}, [normalizedSymbol, livePrice, draft.currentMarketPrice, loadingPrice, supportedSymbols]);
const previewText = useMemo(
() => buildPreviewText(draft, draft.side === 'sell' ? matchingHolding : null),
@ -480,27 +514,37 @@ export function SimpleView() {
async function handleLoadMarketPrice() {
if (!normalizedSymbol) return;
const requestSymbol = normalizedSymbol;
marketPriceRequestSymbolRef.current = requestSymbol;
setLoadingPrice(true);
setError(null);
setMessage(null);
try {
if (livePrice > 0) {
updateDraft('currentMarketPrice', livePrice.toFixed(4));
if (marketPriceRequestSymbolRef.current === requestSymbol) {
updateDraft('currentMarketPrice', livePrice.toFixed(4));
}
return;
}
const bars = await fetchChartBars(normalizedSymbol, '1D');
const bars = await fetchChartBars(requestSymbol, '1D');
const lastClose = Number(bars?.[bars.length - 1]?.close || 0);
if (Number.isFinite(lastClose) && lastClose > 0) {
updateDraft('currentMarketPrice', lastClose.toFixed(4));
if (marketPriceRequestSymbolRef.current === requestSymbol) {
updateDraft('currentMarketPrice', lastClose.toFixed(4));
}
} else {
throw new Error('No recent market data is available for this symbol right now.');
}
} catch (err: any) {
setError(err?.message ?? 'Failed to load market data');
if (marketPriceRequestSymbolRef.current === requestSymbol) {
setError(err?.message ?? 'Failed to load market data');
}
} finally {
setLoadingPrice(false);
if (marketPriceRequestSymbolRef.current === requestSymbol) {
setLoadingPrice(false);
}
}
}