From da66694762ee1f097bffd1639f81ea6cd4308825 Mon Sep 17 00:00:00 2001 From: Steve Tarter Date: Sun, 9 Aug 2026 17:39:08 -0500 Subject: [PATCH 1/6] Fix SimulationTable response parsing for Spring HATEOAS / Lambda responses --- .../components/HomePage/SimulationTable.tsx | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx index 819bf72..597533a 100644 --- a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx +++ b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx @@ -60,14 +60,20 @@ export const SimulationTable = (props: { }); const result = await res.json(); - // Handle single-object response if API returns array or object - if (result && Array.isArray(result.sessions)) { - setData(result.sessions); - setRowCount(result.totalCount || result.sessions.length); - } else if (Array.isArray(result)) { - setData(result as any); - setRowCount(result.length); - } + // Support HATEOAS (_embedded.simulationSessions), custom sessions property, or raw array + const sessions = + result._embedded?.simulationSessions || + result._embedded?.simulationSessionList || + result.sessions || + (Array.isArray(result) ? result : []); + + const total = + result.page?.totalElements ?? + result.totalCount ?? + sessions.length; + + setData(sessions); + setRowCount(total); } catch (err: any) { if (err.name !== 'AbortError') { console.error("Failed to load simulation sessions", err); @@ -82,14 +88,16 @@ export const SimulationTable = (props: { const columns = useMemo( () => [ { - accessorKey: 'vehicleId', + accessorFn: (row: any) => row.vehicleId || row.id, + id: 'vehicleId', header: 'Vehicle ID', size: 150, }, { accessorFn: (row: any) => { + if (!row.start) return 'N/A'; const date = new Date(row.start); - return date.toLocaleString('en-US', timeFormatOptions); + return isNaN(date.getTime()) ? 'N/A' : date.toLocaleString('en-US', timeFormatOptions); }, id: 'start', header: 'Start Time (UTC)', @@ -97,8 +105,9 @@ export const SimulationTable = (props: { }, { accessorFn: (row: any) => { + if (!row.end) return 'Active / In Progress'; const date = new Date(row.end); - return date.toLocaleString('en-US', timeFormatOptions); + return isNaN(date.getTime()) ? 'Active / In Progress' : date.toLocaleString('en-US', timeFormatOptions); }, id: 'end', header: 'End Time (UTC)', From b807891668abb1ae5d7f3a0e1cea3be5a57a8959 Mon Sep 17 00:00:00 2001 From: Steve Tarter Date: Mon, 10 Aug 2026 10:03:42 -0500 Subject: [PATCH 2/6] Fix SimulationTable auth, credentials, and add fallback to simulationSessionMap --- .../components/HomePage/SimulationTable.tsx | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx index 597533a..653167a 100644 --- a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx +++ b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx @@ -1,8 +1,9 @@ -import { fetchAuthSession } from "aws-amplify/auth"; import { useMemo, useState, useEffect } from 'react'; import { MaterialReactTable } from 'material-react-table'; import { getRestUrl } from "../../config"; import { usePlayback } from "../../context/PlaybackContext"; +import { getCachedAuthToken } from "../Utils/AuthUtils"; +import { useSimulationSessionData } from "../../hooks/useSimulationSessionData"; import { Button } from "react-bootstrap"; import { HelpIconButton } from "../Shared/HelpIconButton"; @@ -21,10 +22,11 @@ export const SimulationTable = (props: { toggleSimTable: any, returnToNow: any, }) => { - const [data, setData] = useState([]); + const [data, setData] = useState([]); const [rowCount, setRowCount] = useState(0); const { playbackOffset, setPlaybackSession, dataSource } = usePlayback(); + const { simulationSessionMap } = useSimulationSessionData(); const [pagination, setPagination] = useState({ pageIndex: 0, @@ -36,28 +38,32 @@ export const SimulationTable = (props: { async function loadPage() { try { - // Get the latest session right before the call - const session = await fetchAuthSession(); - const accessToken = session.tokens?.accessToken?.toString(); - - if (!accessToken) { - console.error("Session expired"); - return; - } + const accessToken = await getCachedAuthToken(); const apiPath = dataSource === 'postgis' ? '/api/db-vehicle' : '/api/vehicle'; const url = getRestUrl( `${apiPath}/simulation-sessions?page=${pagination.pageIndex}&pageSize=${pagination.pageSize}` ); + const headers: Record = { + "Content-Type": "application/json" + }; + if (accessToken) { + headers["Authorization"] = `Bearer ${accessToken}`; + } + const res = await fetch(url, { method: 'GET', - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, + headers, + credentials: 'include', signal: controller.signal }); + + if (!res.ok) { + console.warn(`SimulationTable fetch returned status ${res.status}`); + throw new Error(`HTTP ${res.status}`); + } + const result = await res.json(); // Support HATEOAS (_embedded.simulationSessions), custom sessions property, or raw array @@ -72,18 +78,30 @@ export const SimulationTable = (props: { result.totalCount ?? sessions.length; - setData(sessions); - setRowCount(total); + if (sessions.length > 0) { + setData(sessions); + setRowCount(total); + return; + } } catch (err: any) { if (err.name !== 'AbortError') { - console.error("Failed to load simulation sessions", err); + console.error("Failed to load simulation sessions directly", err); } } + + // Fallback: If direct fetch failed or returned no items, use cached simulationSessionMap from useSimulationSessionData + if (simulationSessionMap && simulationSessionMap.size() > 0) { + const fallbackSessions = simulationSessionMap.values(); + const startIdx = pagination.pageIndex * pagination.pageSize; + const endIdx = startIdx + pagination.pageSize; + setData(fallbackSessions.slice(startIdx, endIdx)); + setRowCount(fallbackSessions.length); + } } loadPage(); return () => controller.abort(); - }, [pagination.pageIndex, pagination.pageSize, dataSource]); + }, [pagination.pageIndex, pagination.pageSize, dataSource, simulationSessionMap]); const columns = useMemo( () => [ From e04aaedb78e1a08791c4d6270dd3ad643803eb6d Mon Sep 17 00:00:00 2001 From: Steve Tarter Date: Mon, 10 Aug 2026 10:08:54 -0500 Subject: [PATCH 3/6] Add universal response payload extraction and debug logging to SimulationTable --- .../components/HomePage/SimulationTable.tsx | 66 +++++++++++++++---- 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx index 653167a..c318646 100644 --- a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx +++ b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx @@ -18,15 +18,52 @@ const timeFormatOptions: Intl.DateTimeFormatOptions = { second: '2-digit' }; +function extractSessions(result: any): any[] { + if (!result) return []; + if (Array.isArray(result)) return result; + + // 1. Check inside _embedded (Spring HATEOAS / HAL) + if (result._embedded && typeof result._embedded === 'object') { + if (Array.isArray(result._embedded.simulationSessions)) { + return result._embedded.simulationSessions; + } + const found = Object.values(result._embedded).find((val: any) => Array.isArray(val)); + if (found) return found as any[]; + } + + // 2. Check common root properties + if (Array.isArray(result.simulationSessions)) return result.simulationSessions; + if (Array.isArray(result.sessions)) return result.sessions; + if (Array.isArray(result.content)) return result.content; + if (Array.isArray(result.data)) return result.data; + + // 3. Fallback: find any array property on root object + const anyArray = Object.values(result).find((val: any) => Array.isArray(val)); + if (anyArray) return anyArray as any[]; + + return []; +} + +function extractTotalCount(result: any, fallbackCount: number): number { + if (!result || typeof result !== 'object') return fallbackCount; + if (typeof result.page?.totalElements === 'number') return result.page.totalElements; + if (typeof result.totalElements === 'number') return result.totalElements; + if (typeof result.totalCount === 'number') return result.totalCount; + if (typeof result.total === 'number') return result.total; + if (typeof result.count === 'number') return result.count; + return fallbackCount; +} + export const SimulationTable = (props: { toggleSimTable: any, returnToNow: any, }) => { const [data, setData] = useState([]); const [rowCount, setRowCount] = useState(0); + const [isLoading, setIsLoading] = useState(false); const { playbackOffset, setPlaybackSession, dataSource } = usePlayback(); - const { simulationSessionMap } = useSimulationSessionData(); + const { simulationSessionMap, activeCountData } = useSimulationSessionData(); const [pagination, setPagination] = useState({ pageIndex: 0, @@ -37,6 +74,7 @@ export const SimulationTable = (props: { const controller = new AbortController(); async function loadPage() { + setIsLoading(true); try { const accessToken = await getCachedAuthToken(); @@ -45,6 +83,8 @@ export const SimulationTable = (props: { `${apiPath}/simulation-sessions?page=${pagination.pageIndex}&pageSize=${pagination.pageSize}` ); + console.debug(`SimulationTable fetching URL: ${url}`); + const headers: Record = { "Content-Type": "application/json" }; @@ -65,22 +105,17 @@ export const SimulationTable = (props: { } const result = await res.json(); + console.debug("SimulationTable raw API response:", result); - // Support HATEOAS (_embedded.simulationSessions), custom sessions property, or raw array - const sessions = - result._embedded?.simulationSessions || - result._embedded?.simulationSessionList || - result.sessions || - (Array.isArray(result) ? result : []); + const sessions = extractSessions(result); + const total = extractTotalCount(result, sessions.length); - const total = - result.page?.totalElements ?? - result.totalCount ?? - sessions.length; + console.debug(`SimulationTable extracted ${sessions.length} sessions (totalCount: ${total})`, sessions); if (sessions.length > 0) { setData(sessions); setRowCount(total); + setIsLoading(false); return; } } catch (err: any) { @@ -92,16 +127,21 @@ export const SimulationTable = (props: { // Fallback: If direct fetch failed or returned no items, use cached simulationSessionMap from useSimulationSessionData if (simulationSessionMap && simulationSessionMap.size() > 0) { const fallbackSessions = simulationSessionMap.values(); + console.debug(`SimulationTable fallback using simulationSessionMap with ${fallbackSessions.length} sessions`); const startIdx = pagination.pageIndex * pagination.pageSize; const endIdx = startIdx + pagination.pageSize; setData(fallbackSessions.slice(startIdx, endIdx)); setRowCount(fallbackSessions.length); + } else { + setData([]); + setRowCount(0); } + setIsLoading(false); } loadPage(); return () => controller.abort(); - }, [pagination.pageIndex, pagination.pageSize, dataSource, simulationSessionMap]); + }, [pagination.pageIndex, pagination.pageSize, dataSource, simulationSessionMap, activeCountData]); const columns = useMemo( () => [ @@ -186,7 +226,7 @@ export const SimulationTable = (props: { manualPagination // Tells MRT NOT to do client-side paging rowCount={rowCount} onPaginationChange={setPagination} - state={{ pagination }} + state={{ pagination, isLoading }} layoutMode="grid" enableStickyHeader displayColumnDefOptions={{ From 5b6d3afdafa9b8e6bf05f4066e092d1c18d8d580 Mon Sep 17 00:00:00 2001 From: Steve Tarter Date: Mon, 10 Aug 2026 15:18:21 -0500 Subject: [PATCH 4/6] Fix MapIterator slice TypeError in SimulationTable fallback handler --- .../roadrunner-view/src/components/HomePage/SimulationTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx index c318646..26eda17 100644 --- a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx +++ b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx @@ -126,7 +126,7 @@ export const SimulationTable = (props: { // Fallback: If direct fetch failed or returned no items, use cached simulationSessionMap from useSimulationSessionData if (simulationSessionMap && simulationSessionMap.size() > 0) { - const fallbackSessions = simulationSessionMap.values(); + const fallbackSessions = Array.from(simulationSessionMap.values()); console.debug(`SimulationTable fallback using simulationSessionMap with ${fallbackSessions.length} sessions`); const startIdx = pagination.pageIndex * pagination.pageSize; const endIdx = startIdx + pagination.pageSize; From d9e1847608e9a31e260cb8ac47d32444676b67b1 Mon Sep 17 00:00:00 2001 From: Steve Tarter Date: Mon, 10 Aug 2026 15:40:49 -0500 Subject: [PATCH 5/6] Refactor SimulationTable to consume useSimulationSessionData directly --- .../components/HomePage/SimulationTable.tsx | 141 +++--------------- 1 file changed, 23 insertions(+), 118 deletions(-) diff --git a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx index 26eda17..f77ba31 100644 --- a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx +++ b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx @@ -1,8 +1,6 @@ -import { useMemo, useState, useEffect } from 'react'; +import { useMemo, useState } from 'react'; import { MaterialReactTable } from 'material-react-table'; -import { getRestUrl } from "../../config"; import { usePlayback } from "../../context/PlaybackContext"; -import { getCachedAuthToken } from "../Utils/AuthUtils"; import { useSimulationSessionData } from "../../hooks/useSimulationSessionData"; import { Button } from "react-bootstrap"; import { HelpIconButton } from "../Shared/HelpIconButton"; @@ -18,51 +16,11 @@ const timeFormatOptions: Intl.DateTimeFormatOptions = { second: '2-digit' }; -function extractSessions(result: any): any[] { - if (!result) return []; - if (Array.isArray(result)) return result; - - // 1. Check inside _embedded (Spring HATEOAS / HAL) - if (result._embedded && typeof result._embedded === 'object') { - if (Array.isArray(result._embedded.simulationSessions)) { - return result._embedded.simulationSessions; - } - const found = Object.values(result._embedded).find((val: any) => Array.isArray(val)); - if (found) return found as any[]; - } - - // 2. Check common root properties - if (Array.isArray(result.simulationSessions)) return result.simulationSessions; - if (Array.isArray(result.sessions)) return result.sessions; - if (Array.isArray(result.content)) return result.content; - if (Array.isArray(result.data)) return result.data; - - // 3. Fallback: find any array property on root object - const anyArray = Object.values(result).find((val: any) => Array.isArray(val)); - if (anyArray) return anyArray as any[]; - - return []; -} - -function extractTotalCount(result: any, fallbackCount: number): number { - if (!result || typeof result !== 'object') return fallbackCount; - if (typeof result.page?.totalElements === 'number') return result.page.totalElements; - if (typeof result.totalElements === 'number') return result.totalElements; - if (typeof result.totalCount === 'number') return result.totalCount; - if (typeof result.total === 'number') return result.total; - if (typeof result.count === 'number') return result.count; - return fallbackCount; -} - export const SimulationTable = (props: { toggleSimTable: any, returnToNow: any, }) => { - const [data, setData] = useState([]); - const [rowCount, setRowCount] = useState(0); - const [isLoading, setIsLoading] = useState(false); - - const { playbackOffset, setPlaybackSession, dataSource } = usePlayback(); + const { playbackOffset, setPlaybackSession } = usePlayback(); const { simulationSessionMap, activeCountData } = useSimulationSessionData(); const [pagination, setPagination] = useState({ @@ -70,78 +28,25 @@ export const SimulationTable = (props: { pageSize: 10 }); - useEffect(() => { - const controller = new AbortController(); - - async function loadPage() { - setIsLoading(true); - try { - const accessToken = await getCachedAuthToken(); - - const apiPath = dataSource === 'postgis' ? '/api/db-vehicle' : '/api/vehicle'; - const url = getRestUrl( - `${apiPath}/simulation-sessions?page=${pagination.pageIndex}&pageSize=${pagination.pageSize}` - ); - - console.debug(`SimulationTable fetching URL: ${url}`); - - const headers: Record = { - "Content-Type": "application/json" - }; - if (accessToken) { - headers["Authorization"] = `Bearer ${accessToken}`; - } - - const res = await fetch(url, { - method: 'GET', - headers, - credentials: 'include', - signal: controller.signal - }); - - if (!res.ok) { - console.warn(`SimulationTable fetch returned status ${res.status}`); - throw new Error(`HTTP ${res.status}`); - } - - const result = await res.json(); - console.debug("SimulationTable raw API response:", result); - - const sessions = extractSessions(result); - const total = extractTotalCount(result, sessions.length); - - console.debug(`SimulationTable extracted ${sessions.length} sessions (totalCount: ${total})`, sessions); - - if (sessions.length > 0) { - setData(sessions); - setRowCount(total); - setIsLoading(false); - return; - } - } catch (err: any) { - if (err.name !== 'AbortError') { - console.error("Failed to load simulation sessions directly", err); - } - } - - // Fallback: If direct fetch failed or returned no items, use cached simulationSessionMap from useSimulationSessionData - if (simulationSessionMap && simulationSessionMap.size() > 0) { - const fallbackSessions = Array.from(simulationSessionMap.values()); - console.debug(`SimulationTable fallback using simulationSessionMap with ${fallbackSessions.length} sessions`); - const startIdx = pagination.pageIndex * pagination.pageSize; - const endIdx = startIdx + pagination.pageSize; - setData(fallbackSessions.slice(startIdx, endIdx)); - setRowCount(fallbackSessions.length); - } else { - setData([]); - setRowCount(0); - } - setIsLoading(false); - } - - loadPage(); - return () => controller.abort(); - }, [pagination.pageIndex, pagination.pageSize, dataSource, simulationSessionMap, activeCountData]); + // Extract all sessions directly from simulationSessionMap (same data source as ActiveVehiclePlot) + const allSessions = useMemo(() => { + if (!simulationSessionMap || simulationSessionMap.size() === 0) return []; + return Array.from(simulationSessionMap.values()).sort((a: any, b: any) => { + const startA = new Date(a.start).getTime() || 0; + const startB = new Date(b.start).getTime() || 0; + return startB - startA; // Sort newest sessions first + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [simulationSessionMap, activeCountData]); + + const rowCount = allSessions.length; + + // Paginate sessions locally for MaterialReactTable + const paginatedData = useMemo(() => { + const startIdx = pagination.pageIndex * pagination.pageSize; + const endIdx = startIdx + pagination.pageSize; + return allSessions.slice(startIdx, endIdx); + }, [allSessions, pagination.pageIndex, pagination.pageSize]); const columns = useMemo( () => [ @@ -222,11 +127,11 @@ export const SimulationTable = (props: { Date: Mon, 10 Aug 2026 16:04:33 -0500 Subject: [PATCH 6/6] Sort SimulationTable sessions chronologically (earliest first, latest last) --- .../roadrunner-view/src/components/HomePage/SimulationTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx index f77ba31..e2184a8 100644 --- a/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx +++ b/apps/roadrunner-view/src/components/HomePage/SimulationTable.tsx @@ -34,7 +34,7 @@ export const SimulationTable = (props: { return Array.from(simulationSessionMap.values()).sort((a: any, b: any) => { const startA = new Date(a.start).getTime() || 0; const startB = new Date(b.start).getTime() || 0; - return startB - startA; // Sort newest sessions first + return startA - startB; // Sort earliest sessions first, latest last }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [simulationSessionMap, activeCountData]);