From bdbe68915aa27f1ef3b6e10120363294356b6ae5 Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Mon, 22 Jun 2026 15:03:56 -0400 Subject: [PATCH 01/29] feat: Initial work to create a context providers for warnings --- js/components/app.tsx | 5 ++++- js/contexts/dataWarningsContext.tsx | 20 ++++++++++++++++++++ js/hooks/useChannel.ts | 2 +- js/hooks/useVehicles.ts | 24 ++++++++++++++++++------ 4 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 js/contexts/dataWarningsContext.tsx diff --git a/js/components/app.tsx b/js/components/app.tsx index 47919b2c..5efcdf84 100644 --- a/js/components/app.tsx +++ b/js/components/app.tsx @@ -1,4 +1,5 @@ import { reload } from "../browser"; +import { DataWarningsProvider } from "../contexts/dataWarningsContext"; import { SocketProvider } from "../contexts/socketContext"; import { ORBIT_BL_FFD, @@ -119,7 +120,9 @@ const router = createBrowserRouter([ export const App = (): ReactElement => { return ( - + + + ); }; diff --git a/js/contexts/dataWarningsContext.tsx b/js/contexts/dataWarningsContext.tsx new file mode 100644 index 00000000..d7806a05 --- /dev/null +++ b/js/contexts/dataWarningsContext.tsx @@ -0,0 +1,20 @@ +import { + ReactNode, + createContext, + useContext, + useState, +} from "react"; + +export type DataWarning = {VEHICLE_POSITIONS_STALE: boolean}; + +const DataWarningsContext = createContext(null); +export const DataWarningsProvider = ({ children }: { children: ReactNode }) => { + const [warnings, setWarnings] = useState({VEHICLE_POSITIONS_STALE: false}); + return ( + + {children} + + ); +}; + +export const useDataWarnings = () => useContext(DataWarningsContext); diff --git a/js/hooks/useChannel.ts b/js/hooks/useChannel.ts index 20b68ba7..564cd3be 100644 --- a/js/hooks/useChannel.ts +++ b/js/hooks/useChannel.ts @@ -74,6 +74,6 @@ export const useChannel = ({ event, ); } - }, [topic, event, channel, parser, RawData]); + }, [topic, event, channel, RawData]); return data; }; diff --git a/js/hooks/useVehicles.ts b/js/hooks/useVehicles.ts index 4e209708..30261607 100644 --- a/js/hooks/useVehicles.ts +++ b/js/hooks/useVehicles.ts @@ -6,21 +6,33 @@ import { vehicleFromVehicleData, } from "../models/vehicle"; import { useChannel } from "./useChannel"; - -const parser = (message: VehicleDataMessage): Vehicle[] => { - return message.data.entities.map((data) => vehicleFromVehicleData(data)); -}; +import { useEffect, useState } from "react"; +import { dateTimeFromUnix, useNow } from "../dateTime"; +import { DataWarning, useDataWarnings } from "../contexts/dataWarningsContext"; export const useVehicles = (): Vehicle[] | null => { + const now = useNow("minute"); + const {warnings, setWarnings} = useDataWarnings(); + const [mostRecentTimestamp, setMostRecentTimestamp] = useState(now.toUnixInteger()); const socket = useSocket(); const result = useChannel({ socket, topic: "vehicles", - parser, + parser: (message: VehicleDataMessage): Vehicle[] => { + setMostRecentTimestamp(message.data.timestamp); + return message.data.entities.map((data) => vehicleFromVehicleData(data)); + }, event: "vehicles", RawData: VehicleDataMessage, defaultResult: null, }); + useEffect(() => { + if (now && now.diff(dateTimeFromUnix(mostRecentTimestamp), "minute").minutes > 3) { + setWarnings((warnings: DataWarning) => ({...warnings, VEHICLE_POSITIONS_STALE: true})); + } else { + setWarnings((warnings: DataWarning) => ({...warnings, VEHICLE_POSITIONS_STALE: false})); + } + }, [now]) return result; -}; +}; \ No newline at end of file From 02bd25eb098a4f891bbef3d572f98b1e8e433573 Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Mon, 22 Jun 2026 15:35:06 -0400 Subject: [PATCH 02/29] feat: Basic implementation of warning banner for stale data --- js/components/app.tsx | 2 ++ js/components/banner.tsx | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 js/components/banner.tsx diff --git a/js/components/app.tsx b/js/components/app.tsx index 5efcdf84..16810028 100644 --- a/js/components/app.tsx +++ b/js/components/app.tsx @@ -10,6 +10,7 @@ import { } from "../groups"; import { paths } from "../paths"; import { AppcuesTrackPage } from "./appcues"; +import { Banner } from "./banner"; import { Header } from "./header"; import { LadderPage } from "./ladderPage/ladderPage"; import { LandingPage } from "./landingPage"; @@ -80,6 +81,7 @@ const router = createBrowserRouter([ errorElement: , element: ( <> +
diff --git a/js/components/banner.tsx b/js/components/banner.tsx new file mode 100644 index 00000000..9b4803d2 --- /dev/null +++ b/js/components/banner.tsx @@ -0,0 +1,34 @@ +import { ReactElement } from "react"; +import { useDataWarnings } from "../contexts/dataWarningsContext"; +import { className } from "../util/dom"; + +export const Banner = (): ReactElement => { + const { warnings, setWarnings } = useDataWarnings(); + + return Array.from(Object.values(warnings)).reduce((previous, current) => previous && current) ? ( +
+
+ {""} +

+ Data Issue +

+
+
+
    + {Object.entries(warnings).filter(([key, value]) => value).map(([key, value]) => { + if (key === "VEHICLE_POSITIONS_STALE") { + return
  • Train positions out of date
  • + } + return <> + })} +
+
+
+ ) : <>; +} \ No newline at end of file From e0acd3ff94648449782a52dc63416e5ebccf18cb Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Wed, 24 Jun 2026 09:56:32 -0400 Subject: [PATCH 03/29] chore: Unused variables --- js/components/banner.tsx | 2 +- js/hooks/useVehicles.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/components/banner.tsx b/js/components/banner.tsx index 9b4803d2..cb102a01 100644 --- a/js/components/banner.tsx +++ b/js/components/banner.tsx @@ -3,7 +3,7 @@ import { useDataWarnings } from "../contexts/dataWarningsContext"; import { className } from "../util/dom"; export const Banner = (): ReactElement => { - const { warnings, setWarnings } = useDataWarnings(); + const { warnings } = useDataWarnings(); return Array.from(Object.values(warnings)).reduce((previous, current) => previous && current) ? (
{ const now = useNow("minute"); - const {warnings, setWarnings} = useDataWarnings(); + const { setWarnings } = useDataWarnings(); const [mostRecentTimestamp, setMostRecentTimestamp] = useState(now.toUnixInteger()); const socket = useSocket(); const result = useChannel({ From 3b1046f73fdc99be02525367299b51d1d967e9fa Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Thu, 25 Jun 2026 09:07:20 -0400 Subject: [PATCH 04/29] test: Added DataWarningContext provider to text cases --- js/test/components/app.test.tsx | 21 +++++++++++++-------- js/test/hooks/useVehicles.test.ts | 3 ++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/js/test/components/app.test.tsx b/js/test/components/app.test.tsx index 640cb46d..5fbee592 100644 --- a/js/test/components/app.test.tsx +++ b/js/test/components/app.test.tsx @@ -14,6 +14,7 @@ import { import { getMetaContent } from "../../util/metadata"; import { render, waitFor } from "@testing-library/react"; import { MemoryRouter, Route, Routes } from "react-router"; +import { DataWarningsProvider } from "../../contexts/dataWarningsContext"; jest.mock("../../util/metadata", () => ({ getMetaContent: jest.fn(), @@ -191,10 +192,12 @@ describe("App", () => { const view = render( - - } /> - } /> - + + + } /> + } /> + + , ); @@ -210,10 +213,12 @@ describe("App", () => { const view = render( - - } /> - } /> - + + + } /> + } /> + + , ); diff --git a/js/test/hooks/useVehicles.test.ts b/js/test/hooks/useVehicles.test.ts index 03c7c54a..d952ae1c 100644 --- a/js/test/hooks/useVehicles.test.ts +++ b/js/test/hooks/useVehicles.test.ts @@ -1,3 +1,4 @@ +import { DataWarningsProvider } from "../../contexts/dataWarningsContext"; import { useChannel } from "../../hooks/useChannel"; import { useVehicles } from "../../hooks/useVehicles"; import { renderHook } from "@testing-library/react"; @@ -15,7 +16,7 @@ describe("useVehicles", () => { }); }); test("subscribes to the proper topic", () => { - renderHook(useVehicles); + renderHook(useVehicles, { wrapper: DataWarningsProvider }); expect(mockUseChannel).toHaveBeenCalledWith( expect.objectContaining({ topic: "vehicles", From 7e72f1cf42bc4b338487282356f3cc15df5ddb05 Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Thu, 25 Jun 2026 09:10:40 -0400 Subject: [PATCH 05/29] refactor: Move DataWarningsProvider to wrap the elements rather than the router --- js/components/app.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/js/components/app.tsx b/js/components/app.tsx index 16810028..914ef046 100644 --- a/js/components/app.tsx +++ b/js/components/app.tsx @@ -80,12 +80,12 @@ const router = createBrowserRouter([ { errorElement: , element: ( - <> +
- + ), children: [ { @@ -122,9 +122,7 @@ const router = createBrowserRouter([ export const App = (): ReactElement => { return ( - - - + ); }; From d7003482504435366af196f0763c3938c9d2a66d Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Thu, 25 Jun 2026 09:23:36 -0400 Subject: [PATCH 06/29] refactor: Switch to using a mocked function rather than wrapping the test code in the actual context provider --- js/test/components/app.test.tsx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/js/test/components/app.test.tsx b/js/test/components/app.test.tsx index 5fbee592..48304ea4 100644 --- a/js/test/components/app.test.tsx +++ b/js/test/components/app.test.tsx @@ -14,7 +14,6 @@ import { import { getMetaContent } from "../../util/metadata"; import { render, waitFor } from "@testing-library/react"; import { MemoryRouter, Route, Routes } from "react-router"; -import { DataWarningsProvider } from "../../contexts/dataWarningsContext"; jest.mock("../../util/metadata", () => ({ getMetaContent: jest.fn(), @@ -23,6 +22,11 @@ const mockGetMetaContent = getMetaContent as jest.MockedFunction< typeof getMetaContent >; +jest.mock("../../contexts/dataWarningsContext", () => ({ + __esModule: true, + useDataWarnings: jest.fn(() => ({ warnings: {}, setWarnings: (warnings: any) => {} })), +})); + describe("App", () => { //NOTE: skipping this test while root path temporarily reroutes to /operators. //TODO: do not skip this once root path is no longer just rerouting. @@ -192,12 +196,10 @@ describe("App", () => { const view = render( - - - } /> - } /> - - + + } /> + } /> + , ); @@ -213,12 +215,10 @@ describe("App", () => { const view = render( - - - } /> - } /> - - + + } /> + } /> + , ); From 438130c8e6e94b94bf7b8c029e73b3f3643dadeb Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Thu, 25 Jun 2026 11:07:54 -0400 Subject: [PATCH 07/29] refactor: Cleanup conditional logic for setting stale --- js/hooks/useVehicles.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/js/hooks/useVehicles.ts b/js/hooks/useVehicles.ts index c949f29e..c1e15b3a 100644 --- a/js/hooks/useVehicles.ts +++ b/js/hooks/useVehicles.ts @@ -27,11 +27,12 @@ export const useVehicles = (): Vehicle[] | null => { defaultResult: null, }); useEffect(() => { - if (now && now.diff(dateTimeFromUnix(mostRecentTimestamp), "minute").minutes > 3) { - setWarnings((warnings: DataWarning) => ({...warnings, VEHICLE_POSITIONS_STALE: true})); - } else { - setWarnings((warnings: DataWarning) => ({...warnings, VEHICLE_POSITIONS_STALE: false})); - } + setWarnings((warnings: DataWarning) => ({ + ...warnings, + VEHICLE_POSITIONS_STALE: now && now.diff( + dateTimeFromUnix(mostRecentTimestamp), "minute" + ).minutes > 3 + })); }, [now]) return result; From fbd3c133e95459532d4a979091c7e7bc86fbb869 Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Thu, 25 Jun 2026 13:46:35 -0400 Subject: [PATCH 08/29] test: Banner test cases --- js/test/components/app.test.tsx | 5 ++- js/test/components/banner.test.tsx | 51 ++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 js/test/components/banner.test.tsx diff --git a/js/test/components/app.test.tsx b/js/test/components/app.test.tsx index 48304ea4..e258817a 100644 --- a/js/test/components/app.test.tsx +++ b/js/test/components/app.test.tsx @@ -24,7 +24,10 @@ const mockGetMetaContent = getMetaContent as jest.MockedFunction< jest.mock("../../contexts/dataWarningsContext", () => ({ __esModule: true, - useDataWarnings: jest.fn(() => ({ warnings: {}, setWarnings: (warnings: any) => {} })), + useDataWarnings: jest.fn(() => ({ + warnings: {}, + setWarnings: (warnings: any) => {}, + })), })); describe("App", () => { diff --git a/js/test/components/banner.test.tsx b/js/test/components/banner.test.tsx new file mode 100644 index 00000000..858b9b4f --- /dev/null +++ b/js/test/components/banner.test.tsx @@ -0,0 +1,51 @@ +import { Banner } from "../../components/banner"; +import { useDataWarnings } from "../../contexts/dataWarningsContext"; +import { render } from "@testing-library/react"; +import { MemoryRouter } from "react-router"; + +jest.mock("../../contexts/dataWarningsContext", () => ({ + __esModule: true, + useDataWarnings: jest.fn(), +})); + +describe("Landing Page", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test("renders when warning is present", () => { + (useDataWarnings as jest.Mock).mockImplementation( + jest.fn(() => ({ + warnings: { VEHICLE_POSITIONS_STALE: true }, + setWarnings: (warnings: any) => {}, + })), + ); + + const view = render( + + + , + ); + expect(view.getByText("Data Issue")).toBeInTheDocument(); + expect(view.getByText("Train positions out of date")).toBeInTheDocument(); + }); + + test("is hidden when no warning is present", () => { + (useDataWarnings as jest.Mock).mockImplementation( + jest.fn(() => ({ + warnings: { VEHICLE_POSITIONS_STALE: false }, + setWarnings: (warnings: any) => {}, + })), + ); + + const view = render( + + + , + ); + expect(view.queryByText("Data Issue")).not.toBeInTheDocument(); + expect( + view.queryByText("Train positions out of date"), + ).not.toBeInTheDocument(); + }); +}); From 08f851506fefe454109b72560f46b7aa9bb9cf10 Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Thu, 25 Jun 2026 14:04:44 -0400 Subject: [PATCH 09/29] refactor: Change to using array destructuring for state values --- js/components/banner.tsx | 2 +- js/contexts/dataWarningsContext.tsx | 15 ++++++--------- js/hooks/useVehicles.ts | 20 +++++++++++--------- js/test/components/banner.test.tsx | 16 ++++++++-------- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/js/components/banner.tsx b/js/components/banner.tsx index cb102a01..2c0ad619 100644 --- a/js/components/banner.tsx +++ b/js/components/banner.tsx @@ -3,7 +3,7 @@ import { useDataWarnings } from "../contexts/dataWarningsContext"; import { className } from "../util/dom"; export const Banner = (): ReactElement => { - const { warnings } = useDataWarnings(); + const [warnings] = useDataWarnings(); return Array.from(Object.values(warnings)).reduce((previous, current) => previous && current) ? (
(null); export const DataWarningsProvider = ({ children }: { children: ReactNode }) => { - const [warnings, setWarnings] = useState({VEHICLE_POSITIONS_STALE: false}); + const [warnings, setWarnings] = useState({ + VEHICLE_POSITIONS_STALE: false, + }); return ( - + {children} ); diff --git a/js/hooks/useVehicles.ts b/js/hooks/useVehicles.ts index c1e15b3a..b51be529 100644 --- a/js/hooks/useVehicles.ts +++ b/js/hooks/useVehicles.ts @@ -1,5 +1,7 @@ import { useSocket } from "../contexts/socketContext"; import "../models/vehiclePosition"; +import { DataWarning, useDataWarnings } from "../contexts/dataWarningsContext"; +import { dateTimeFromUnix, useNow } from "../dateTime"; import { Vehicle, VehicleDataMessage, @@ -7,13 +9,13 @@ import { } from "../models/vehicle"; import { useChannel } from "./useChannel"; import { useEffect, useState } from "react"; -import { dateTimeFromUnix, useNow } from "../dateTime"; -import { DataWarning, useDataWarnings } from "../contexts/dataWarningsContext"; export const useVehicles = (): Vehicle[] | null => { const now = useNow("minute"); - const { setWarnings } = useDataWarnings(); - const [mostRecentTimestamp, setMostRecentTimestamp] = useState(now.toUnixInteger()); + const [, setWarnings] = useDataWarnings(); + const [mostRecentTimestamp, setMostRecentTimestamp] = useState( + now.toUnixInteger(), + ); const socket = useSocket(); const result = useChannel({ socket, @@ -29,11 +31,11 @@ export const useVehicles = (): Vehicle[] | null => { useEffect(() => { setWarnings((warnings: DataWarning) => ({ ...warnings, - VEHICLE_POSITIONS_STALE: now && now.diff( - dateTimeFromUnix(mostRecentTimestamp), "minute" - ).minutes > 3 + VEHICLE_POSITIONS_STALE: + now && + now.diff(dateTimeFromUnix(mostRecentTimestamp), "minute").minutes > 3, })); - }, [now]) + }, [now]); return result; -}; \ No newline at end of file +}; diff --git a/js/test/components/banner.test.tsx b/js/test/components/banner.test.tsx index 858b9b4f..a62a6b28 100644 --- a/js/test/components/banner.test.tsx +++ b/js/test/components/banner.test.tsx @@ -15,10 +15,10 @@ describe("Landing Page", () => { test("renders when warning is present", () => { (useDataWarnings as jest.Mock).mockImplementation( - jest.fn(() => ({ - warnings: { VEHICLE_POSITIONS_STALE: true }, - setWarnings: (warnings: any) => {}, - })), + jest.fn(() => ([ + { VEHICLE_POSITIONS_STALE: true }, + (warnings: any) => {}, + ])), ); const view = render( @@ -32,10 +32,10 @@ describe("Landing Page", () => { test("is hidden when no warning is present", () => { (useDataWarnings as jest.Mock).mockImplementation( - jest.fn(() => ({ - warnings: { VEHICLE_POSITIONS_STALE: false }, - setWarnings: (warnings: any) => {}, - })), + jest.fn(() => ([ + { VEHICLE_POSITIONS_STALE: false }, + (warnings: any) => {}, + ])), ); const view = render( From 5bdb7f92165eea18ce70e6778f90753ca41d15bb Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Thu, 25 Jun 2026 14:08:29 -0400 Subject: [PATCH 10/29] fix: Formatting + removing references to dark/light mode in class names --- js/components/banner.tsx | 58 +++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/js/components/banner.tsx b/js/components/banner.tsx index 2c0ad619..03e813ed 100644 --- a/js/components/banner.tsx +++ b/js/components/banner.tsx @@ -1,34 +1,38 @@ -import { ReactElement } from "react"; import { useDataWarnings } from "../contexts/dataWarningsContext"; import { className } from "../util/dom"; +import { ReactElement } from "react"; export const Banner = (): ReactElement => { const [warnings] = useDataWarnings(); - return Array.from(Object.values(warnings)).reduce((previous, current) => previous && current) ? ( -
-
- {""} -

- Data Issue -

-
-
-
    - {Object.entries(warnings).filter(([key, value]) => value).map(([key, value]) => { - if (key === "VEHICLE_POSITIONS_STALE") { - return
  • Train positions out of date
  • - } - return <> - })} -
-
+ return ( + Array.from(Object.values(warnings)).reduce( + (previous, current) => previous && current, + ) + ) ? +
+
+ + + +

+ Data Issue +

+
+
+
    + {Object.entries(warnings) + .filter(([key, value]) => value) + .map(([key, value]) => { + if (key === "VEHICLE_POSITIONS_STALE") { + return
  • Train positions out of date
  • ; + } + return <>; + })} +
- ) : <>; -} \ No newline at end of file +
+ : <>; +}; From 33067e2ea4aac3b8ddd00d2453c350b6bd0f6416 Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Thu, 25 Jun 2026 14:38:16 -0400 Subject: [PATCH 11/29] refactor: Switch to using a Set for warnings and hide the implementation details in the context --- js/components/banner.tsx | 20 ++++++----------- js/contexts/dataWarningsContext.tsx | 34 ++++++++++++++++++++++++----- js/hooks/useVehicles.ts | 16 ++++++++------ js/test/components/banner.test.tsx | 12 +++++----- 4 files changed, 49 insertions(+), 33 deletions(-) diff --git a/js/components/banner.tsx b/js/components/banner.tsx index 03e813ed..d776b0fd 100644 --- a/js/components/banner.tsx +++ b/js/components/banner.tsx @@ -5,11 +5,7 @@ import { ReactElement } from "react"; export const Banner = (): ReactElement => { const [warnings] = useDataWarnings(); - return ( - Array.from(Object.values(warnings)).reduce( - (previous, current) => previous && current, - ) - ) ? + return warnings.size > 0 ?
@@ -23,14 +19,12 @@ export const Banner = (): ReactElement => {
    - {Object.entries(warnings) - .filter(([key, value]) => value) - .map(([key, value]) => { - if (key === "VEHICLE_POSITIONS_STALE") { - return
  • Train positions out of date
  • ; - } - return <>; - })} + {[...warnings].map((warning) => { + if (warning === "vehicle_positions_stale") { + return
  • Train positions out of date
  • ; + } + return <>; + })}
diff --git a/js/contexts/dataWarningsContext.tsx b/js/contexts/dataWarningsContext.tsx index 85118870..8e19481f 100644 --- a/js/contexts/dataWarningsContext.tsx +++ b/js/contexts/dataWarningsContext.tsx @@ -1,14 +1,36 @@ import { createContext, ReactNode, useContext, useState } from "react"; -export type DataWarning = { VEHICLE_POSITIONS_STALE: boolean }; +export type DataWarning = "vehicle_positions_stale"; +export type DataWarnings = Set; -const DataWarningsContext = createContext(null); +const DataWarningsContext = createContext< + [DataWarnings, (warning: DataWarning) => void, (warning: DataWarning) => void] +>([ + new Set(), + (warning) => { + throw Error("Not implemented"); + }, + (warning) => { + throw Error("Not implemented"); + }, +]); export const DataWarningsProvider = ({ children }: { children: ReactNode }) => { - const [warnings, setWarnings] = useState({ - VEHICLE_POSITIONS_STALE: false, - }); + const [warnings, setWarnings] = useState(new Set()); + const addWarning: (warning: DataWarning) => void = (warning: DataWarning) => { + setWarnings((warnings) => warnings.add(warning)); + }; + const removeWarning: (warning: DataWarning) => void = ( + warning: DataWarning, + ) => { + setWarnings((warnings) => { + const newSet = new Set(warnings); + newSet.delete(warning); + return newSet; + }); + }; + return ( - + {children} ); diff --git a/js/hooks/useVehicles.ts b/js/hooks/useVehicles.ts index b51be529..771e285d 100644 --- a/js/hooks/useVehicles.ts +++ b/js/hooks/useVehicles.ts @@ -12,7 +12,7 @@ import { useEffect, useState } from "react"; export const useVehicles = (): Vehicle[] | null => { const now = useNow("minute"); - const [, setWarnings] = useDataWarnings(); + const [, addWarning, removeWarning] = useDataWarnings(); const [mostRecentTimestamp, setMostRecentTimestamp] = useState( now.toUnixInteger(), ); @@ -29,12 +29,14 @@ export const useVehicles = (): Vehicle[] | null => { defaultResult: null, }); useEffect(() => { - setWarnings((warnings: DataWarning) => ({ - ...warnings, - VEHICLE_POSITIONS_STALE: - now && - now.diff(dateTimeFromUnix(mostRecentTimestamp), "minute").minutes > 3, - })); + if ( + now && + now.diff(dateTimeFromUnix(mostRecentTimestamp), "minute").minutes > 3 + ) { + addWarning("vehicle_positions_stale"); + } else { + removeWarning("vehicle_positions_stale"); + } }, [now]); return result; diff --git a/js/test/components/banner.test.tsx b/js/test/components/banner.test.tsx index a62a6b28..ff1c520f 100644 --- a/js/test/components/banner.test.tsx +++ b/js/test/components/banner.test.tsx @@ -15,10 +15,11 @@ describe("Landing Page", () => { test("renders when warning is present", () => { (useDataWarnings as jest.Mock).mockImplementation( - jest.fn(() => ([ - { VEHICLE_POSITIONS_STALE: true }, + jest.fn(() => [ + new Set(["vehicle_positions_stale"]), (warnings: any) => {}, - ])), + (warnings: any) => {}, + ]), ); const view = render( @@ -32,10 +33,7 @@ describe("Landing Page", () => { test("is hidden when no warning is present", () => { (useDataWarnings as jest.Mock).mockImplementation( - jest.fn(() => ([ - { VEHICLE_POSITIONS_STALE: false }, - (warnings: any) => {}, - ])), + jest.fn(() => [new Set(), (warnings: any) => {}, (warnings: any) => {}]), ); const view = render( From 765f31b482c1b85773664d087d93d73a69ef6f52 Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Thu, 25 Jun 2026 14:42:58 -0400 Subject: [PATCH 12/29] test: Missed a mock update for app test --- js/test/components/app.test.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/js/test/components/app.test.tsx b/js/test/components/app.test.tsx index e258817a..552fcb23 100644 --- a/js/test/components/app.test.tsx +++ b/js/test/components/app.test.tsx @@ -24,10 +24,11 @@ const mockGetMetaContent = getMetaContent as jest.MockedFunction< jest.mock("../../contexts/dataWarningsContext", () => ({ __esModule: true, - useDataWarnings: jest.fn(() => ({ - warnings: {}, - setWarnings: (warnings: any) => {}, - })), + useDataWarnings: jest.fn(() => [ + new Set([]), + (warnings: any) => {}, + (warnings: any) => {}, + ]), })); describe("App", () => { From 5c64853cb3967123bd08cbc259e298448bd321bc Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Thu, 25 Jun 2026 14:52:52 -0400 Subject: [PATCH 13/29] fix: Mutating Set on add warning was causing failure to update --- js/contexts/dataWarningsContext.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/contexts/dataWarningsContext.tsx b/js/contexts/dataWarningsContext.tsx index 8e19481f..d71565e0 100644 --- a/js/contexts/dataWarningsContext.tsx +++ b/js/contexts/dataWarningsContext.tsx @@ -17,7 +17,11 @@ const DataWarningsContext = createContext< export const DataWarningsProvider = ({ children }: { children: ReactNode }) => { const [warnings, setWarnings] = useState(new Set()); const addWarning: (warning: DataWarning) => void = (warning: DataWarning) => { - setWarnings((warnings) => warnings.add(warning)); + setWarnings((warnings) => { + const newSet = new Set(warnings); + newSet.add(warning); + return newSet; + }); }; const removeWarning: (warning: DataWarning) => void = ( warning: DataWarning, From e6aa229c13e32391019797a4ff3ef77a4e0e147b Mon Sep 17 00:00:00 2001 From: Ryan Collins Date: Fri, 26 Jun 2026 11:09:25 -0400 Subject: [PATCH 14/29] fix: Added icon and adjusted styling to address offsets caused by the warning banner --- js/components/banner.tsx | 4 ++-- js/components/header.tsx | 2 +- js/components/ladderPage/ladderPage.tsx | 6 +++--- js/components/ladderPage/sidebar.tsx | 2 +- js/icons.ts | 3 ++- priv/static/images/warning-circle.svg | 1 + 6 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 priv/static/images/warning-circle.svg diff --git a/js/components/banner.tsx b/js/components/banner.tsx index d776b0fd..3d4d9fda 100644 --- a/js/components/banner.tsx +++ b/js/components/banner.tsx @@ -10,8 +10,8 @@ export const Banner = (): ReactElement => { className={className(["flex flex-col px-3 py-4 text-xs bg-yellow/25"])} >
- - + +

Data Issue diff --git a/js/components/header.tsx b/js/components/header.tsx index 688bf3b7..05e2fbfd 100644 --- a/js/components/header.tsx +++ b/js/components/header.tsx @@ -9,7 +9,7 @@ export const Header = () => { currentLocation.pathname === paths.help; return ( -

+
MBTA diff --git a/js/components/ladderPage/ladderPage.tsx b/js/components/ladderPage/ladderPage.tsx index 7be5a76b..dbc6b1b8 100644 --- a/js/components/ladderPage/ladderPage.tsx +++ b/js/components/ladderPage/ladderPage.tsx @@ -45,6 +45,9 @@ export const LadderPage = ({ routeId }: { routeId: RouteId }): ReactElement => { return (
+ {sideBarSelection !== null ? + + : null}
{ sideBarSelection={sideBarSelection} />
- {sideBarSelection !== null ? - - : null}
); }; diff --git a/js/components/ladderPage/sidebar.tsx b/js/components/ladderPage/sidebar.tsx index b235d62a..c9768554 100644 --- a/js/components/ladderPage/sidebar.tsx +++ b/js/components/ladderPage/sidebar.tsx @@ -27,7 +27,7 @@ export const SideBar = ({ close: () => void; }): ReactElement => { return ( -