diff --git a/src/components/StrategyTimeline.tsx b/src/components/StrategyTimeline.tsx index a39074d7..56b1d2c4 100644 --- a/src/components/StrategyTimeline.tsx +++ b/src/components/StrategyTimeline.tsx @@ -18,6 +18,19 @@ type StrategyRun = { events: StrategyEvent[]; }; +type StrategySummary = { + run_id?: string | null; + status?: string | null; + tone?: "neutral" | "warning" | "error" | "success"; + message?: string | null; + event?: string | null; + ts?: string | null; +}; + +type StrategyTimelineProps = { + compact?: boolean; +}; + function normalizeLog(log: unknown): StrategyEvent { if (typeof log === "string") { try { @@ -45,6 +58,16 @@ function formatTimestamp(value?: string) { return parsed.toLocaleString(); } +function formatCompactTimestamp(value?: string | null) { + if (!value) return null; + const parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) return null; + return parsed.toLocaleTimeString([], { + hour: "numeric", + minute: "2-digit", + }); +} + function getRunId(entry: StrategyEvent) { return entry.run_id ?? "unknown"; } @@ -104,7 +127,62 @@ function getStopReason(events: StrategyEvent[]) { return typeof reason === "string" ? reason : null; } -export default function StrategyTimeline() { +function CompactStrategySummary() { + const [summary, setSummary] = useState(null); + + useEffect(() => { + let cancelled = false; + + const fetchSummary = async () => { + try { + const res = await apiRequest("GET", "/strategy/summary"); + const data = (await res.json()) as StrategySummary; + if (!cancelled) { + setSummary(data); + } + } catch { + if (!cancelled) { + setSummary({ + tone: "error", + message: "Could not load strategy status.", + }); + } + } + }; + + fetchSummary(); + const id = window.setInterval(fetchSummary, 5000); + return () => { + cancelled = true; + window.clearInterval(id); + }; + }, []); + + const tone = summary?.tone ?? "neutral"; + const message = summary?.message || "No active strategy."; + const updatedAt = formatCompactTimestamp(summary?.ts); + const className = + tone === "error" + ? "border-red-500/40 bg-red-500/10 text-red-300" + : tone === "warning" + ? "border-amber-400/40 bg-amber-400/10 text-amber-200" + : tone === "success" + ? "border-emerald-500/40 bg-emerald-500/10 text-emerald-300" + : "border-border/60 bg-background/40 text-muted-foreground"; + + return ( +
+
+ {message} + {updatedAt ? ( + Updated {updatedAt} + ) : null} +
+
+ ); +} + +function VerboseStrategyTimeline() { const [logs, setLogs] = useState([]); const latestSeqRef = useRef(0); @@ -118,10 +196,10 @@ export default function StrategyTimeline() { setLogs((prev) => { const seenSeq = new Set( - prev.map((entry) => entry.seq).filter((seq) => typeof seq === "number") + prev.map((entry) => entry.seq).filter((seq) => typeof seq === "number"), ); const next = normalized.filter( - (entry) => typeof entry.seq !== "number" || !seenSeq.has(entry.seq) + (entry) => typeof entry.seq !== "number" || !seenSeq.has(entry.seq), ); return [...prev, ...next]; }); @@ -131,7 +209,7 @@ export default function StrategyTimeline() { } else { const lastSeq = normalized.reduce( (max, entry) => Math.max(max, entry.seq ?? 0), - latestSeqRef.current + latestSeqRef.current, ); latestSeqRef.current = lastSeq; } @@ -190,7 +268,7 @@ export default function StrategyTimeline() {
- ▶ Run started at {formatTimestamp(startTime)} + Run started at {formatTimestamp(startTime)}
Run ID: {run.runId}
@@ -264,3 +342,10 @@ export default function StrategyTimeline() {
); } + +export default function StrategyTimeline({ compact = false }: StrategyTimelineProps) { + if (compact) { + return ; + } + return ; +} diff --git a/src/components/landing/BrokerConnectDialog.tsx b/src/components/landing/BrokerConnectDialog.tsx index 3d98da0a..c90c30f0 100644 --- a/src/components/landing/BrokerConnectDialog.tsx +++ b/src/components/landing/BrokerConnectDialog.tsx @@ -32,6 +32,7 @@ type BrokerStatusResponse = { connected_at?: string; userName?: string; brokerUserId?: string; + authState?: string; }; const CALLBACK_STORAGE_KEY = "zerodha:callback"; @@ -61,9 +62,11 @@ export default function BrokerConnectDialog({ const [apiSecret, setApiSecret] = useState(""); const [holdings, setHoldings] = useState([]); - const { data: sessionUser } = useQuery({ + const { data: sessionUser, refetch: refetchSessionUser } = useQuery({ queryKey: ["/me"], queryFn: getQueryFn({ on401: "returnNull" }), + staleTime: 0, + refetchOnMount: "always", }); const { data: brokerStatus, refetch: refetchStatus } = useQuery({ @@ -100,6 +103,34 @@ export default function BrokerConnectDialog({ toast({ title: "Could not start Zerodha login", description: err?.message || "Try again." }), }); + const reconnectSavedMutation = useMutation({ + mutationFn: async () => { + const res = await apiRequest("GET", "/broker/login-url"); + return res.json() as Promise<{ loginUrl: string }>; + }, + onSuccess: ({ loginUrl }) => { + window.open(loginUrl, "_blank", "noopener,noreferrer"); + toast({ + title: "Continue in Zerodha", + description: "Log in and return here. We will reconnect your broker automatically.", + }); + }, + onError: (err: any) => { + const message = String(err?.message || ""); + if (message.includes("400:") && message.includes("Broker credentials not configured")) { + toast({ + title: "Enter Zerodha API credentials", + description: "Saved credentials are missing. Enter the API key and secret once to reconnect.", + }); + return; + } + toast({ + title: "Could not reconnect Zerodha", + description: err?.message || "Try again.", + }); + }, + }); + const holdingsMutation = useMutation({ mutationFn: async () => { const res = await apiRequest("GET", "/zerodha/holdings"); @@ -133,12 +164,15 @@ export default function BrokerConnectDialog({ }); const connected = !!brokerStatus?.connected; + const canReconnectWithSavedZerodha = + connected && (brokerStatus?.broker || "").trim().toUpperCase() === "ZERODHA"; const connectedAt = brokerStatus?.connected_at ? new Date(brokerStatus.connected_at) : null; - const handleConnectClick = () => { - if (!sessionUser) { + const handleConnectClick = async () => { + const latest = await refetchSessionUser(); + if (!latest.data) { setLoginPromptOpen(true); return; } @@ -297,6 +331,16 @@ export default function BrokerConnectDialog({ {loginUrlMutation.isPending ? "Opening Zerodha..." : "Open Zerodha login"} + {canReconnectWithSavedZerodha ? ( + + ) : null} {connected ? ( ) : null} + {isConnected ? ( + + ) : null} + {isConnected ? ( + + ) : null} @@ -887,109 +919,6 @@ export default function PortfolioSection() { )} -
-
-
-

System arm

-

- Re-arm all active strategies after broker login. -

-
- -
-
-
-

Broker

-

- {isConnected ? "Connected" : "Not connected"} -

-
-
-

Armed

-

{armedCount}

-
-
-

Market

-

{marketState}

-
-
-

Next execution

-

- {nextExecution ? formatMinuteTimestamp(nextExecution) : "Unknown"} -

-
-
-

Engine

-

{liveness}

-
-
- {armSummary ? ( -
- System armed. {armSummary.failed_runs?.length ? "Some runs failed to arm." : "All runs armed."} -
- ) : null} -
- -
-
-
-

Strategy status

-

- Live status for every configured strategy run. -

-
- {systemRuns.length} total -
- {systemStatusQuery.isLoading ? ( - - ) : systemRuns.length === 0 ? ( - - ) : ( -
- - - - - - - - - - - - - {systemRuns.map((run) => ( - - - - - - - - - ))} - -
StrategyModeStatusNext runBrokerLifecycle
- {run.strategy || "Strategy"} - {run.mode || "-"}{run.status} - {run.next_run ? new Date(run.next_run).toLocaleString() : "-"} - {run.broker || "-"}{run.lifecycle || run.status}
-
- )} -
- {marketState === "CLOSED" ? ( -

- Market closed — execution will resume at next session -

- ) : null} - {isStrategyRunning ? ( -

- Strategy running — next SIP will execute when eligible -

- ) : null} + - -