From ffbc46f46350995e30fa05852d051b3bfddbe43e Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Thu, 6 Aug 2026 06:01:01 +0200 Subject: [PATCH] fix(dashboard): Previous/Next nav closing modal on stale background list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Background list polling intentionally pauses while a request's detail modal is open, so hitting the edge of the in-memory sorted list didn't mean there was really nothing newer/older — it just meant the client hadn't fetched requests that landed in the background yet. handlePrev/handleNext now resync the list once at that boundary and let a follow-up effect decide whether to navigate or actually close, instead of assuming the boundary is real. Also removes onNavigateToLog from RequestLoggerV2/RequestTimeline's calls into RequestLoggerDetail — that prop no longer exists after the detail panel's cross-row next-turn navigation was removed in the prior commit. --- src/shared/components/RequestLoggerV2.tsx | 52 +++++++++++++-- src/shared/components/RequestTimeline.tsx | 77 ++++++++++++----------- 2 files changed, 90 insertions(+), 39 deletions(-) diff --git a/src/shared/components/RequestLoggerV2.tsx b/src/shared/components/RequestLoggerV2.tsx index 5d9f4236ac..670c9dc422 100644 --- a/src/shared/components/RequestLoggerV2.tsx +++ b/src/shared/components/RequestLoggerV2.tsx @@ -188,6 +188,12 @@ const RequestLoggerV2 = forwardRef(null); const [visibleColumns, setVisibleColumns] = useState(() => { const defaultVisible = Object.fromEntries(columns.map((c) => [c.key, true])); @@ -750,9 +756,14 @@ const RequestLoggerV2 = forwardRef { const idx = currentLogIndex; @@ -764,10 +775,44 @@ const RequestLoggerV2 = forwardRef { console.error("Failed to open previous log id:", error_); }); + } else { + pendingBoundaryNavRef.current = "next"; + fetchLogs(false); + } + }, [currentLogIndex, sortedLogsForNav, fetchLogs]); + + // Resolves a pending boundary nav (see handlePrev/handleNext) once a + // triggered fetchLogs() resync has landed in sortedLogsForNav. Only fires + // when a boundary nav is actually pending, so this is a no-op on the + // normal (paused-while-modal-open) list-update cadence. + useEffect(() => { + const direction = pendingBoundaryNavRef.current; + if (!direction || !selectedLog) return; + pendingBoundaryNavRef.current = null; + const idx = sortedLogsForNav.findIndex((l) => l.id === selectedLog.id); + const target = + direction === "prev" + ? idx > 0 + ? sortedLogsForNav[idx - 1] + : null + : idx >= 0 && idx < sortedLogsForNav.length - 1 + ? sortedLogsForNav[idx + 1] + : null; + if (target?.id) { + openDetail(target) + .then((r) => r) + .catch((error_) => { + console.error("Failed to open adjacent log id:", error_); + }); } else { closeDetail(); } - }, [currentLogIndex, sortedLogsForNav]); + // openDetail/closeDetail are plain functions re-created every render + // (same as handlePrev/handleNext above and the rest of this file) — + // listing them would re-fire this effect on every render instead of + // only when sortedLogsForNav/selectedLog actually change. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sortedLogsForNav, selectedLog]); const toggleDetailLogging = async () => { setDetailLoggingLoading(true); @@ -1629,7 +1674,6 @@ const RequestLoggerV2 = forwardRef openDetail({ id })} /> )} diff --git a/src/shared/components/RequestTimeline.tsx b/src/shared/components/RequestTimeline.tsx index 165ca291cc..27613b63a2 100644 --- a/src/shared/components/RequestTimeline.tsx +++ b/src/shared/components/RequestTimeline.tsx @@ -481,40 +481,43 @@ export default function RequestTimeline({ // Deep-link support: open the request from ?id= on mount without waiting for // it to show up in the polled `logs` list (mirrors RequestLoggerV2's openDetail). - const openById = useCallback(async (id: string) => { - try { - const url = new URL(globalThis.location.href); - url.searchParams.set("id", id); - router.replace(url.pathname + url.search); - } catch { - // ignore navigation errors - } - setDetailLoading(true); - try { - const res = await fetch(`/api/logs/${id}`, { cache: "no-store" }); - const data = res.ok ? await res.json() : null; - if (data) { - setSelectedLog({ - id: data.id ?? id, - timestamp: data.timestamp, - status: data.status ?? 0, - model: data.model ?? null, - provider: data.provider ?? null, - account: data.account ?? null, - duration: data.duration ?? 0, - tokens: data.tokens ?? { in: 0, out: 0 }, - active: data.active, - error: data.error ?? null, - path: data.path ?? null, - }); - setDetailData(data); + const openById = useCallback( + async (id: string) => { + try { + const url = new URL(globalThis.location.href); + url.searchParams.set("id", id); + router.replace(url.pathname + url.search); + } catch { + // ignore navigation errors } - } catch { - // ignore fetch errors - } finally { - setDetailLoading(false); - } - }, [router]); + setDetailLoading(true); + try { + const res = await fetch(`/api/logs/${id}`, { cache: "no-store" }); + const data = res.ok ? await res.json() : null; + if (data) { + setSelectedLog({ + id: data.id ?? id, + timestamp: data.timestamp, + status: data.status ?? 0, + model: data.model ?? null, + provider: data.provider ?? null, + account: data.account ?? null, + duration: data.duration ?? 0, + tokens: data.tokens ?? { in: 0, out: 0 }, + active: data.active, + error: data.error ?? null, + path: data.path ?? null, + }); + setDetailData(data); + } + } catch { + // ignore fetch errors + } finally { + setDetailLoading(false); + } + }, + [router] + ); useEffect(() => { if (!initialSelectedId || initialOpenedRef.current) return; @@ -832,7 +835,12 @@ export default function RequestTimeline({ bar pair sharing a lane. */} @@ -1028,7 +1036,6 @@ export default function RequestTimeline({ onNext={undefined} relatedLogs={[]} onSelectRelated={undefined} - onNavigateToLog={openById} /> )}