feat: CVSR live train tracker with GPS tracking#530
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Polls the US Fleet Tracking API for the Cuyahoga Valley Scenic Railroad train's GPS position and renders it as a moving icon on the map. The train snaps to the OSM railroad track geometry and rotates to follow the track bearing. Key features: - Backend service polls USFT API every 5s with JWT auth - Frontend polls backend every 5s for near-real-time position updates - 704-point track geometry from OSM (Akron to Rockside) - Train icon snaps to nearest point on track with computed bearing - ?feature=CVSR URL flag gates visibility for partner demos - Permalink /cuyahoga-valley-scenic-railroad zooms to train position - Click track or train icon to open CVSR sidebar - Renamed "Train Station" legend entry to "Train" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces live GPS tracking for the Cuyahoga Valley Scenic Railroad (CVSR) train, adding a backend polling service to fetch coordinates from the US Fleet Tracking API and a frontend map marker that snaps to the railroad tracks. The feature is gated behind a 'CVSR' feature flag. The review feedback highlights several critical robustness and performance improvements, such as conditionally enabling the frontend polling hook only when the feature flag is active, defensively validating coordinates to prevent Leaflet map crashes, implementing concurrency locks for backend polling, handling initial API authentication failures gracefully, and wrapping localStorage calls in try-catch blocks to prevent crashes in restricted environments.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export default function useTrainPosition() { | ||
| const [trainPosition, setTrainPosition] = useState(null); | ||
|
|
||
| useEffect(() => { | ||
| let mounted = true; | ||
|
|
||
| const fetchPosition = () => { | ||
| fetch('/api/train/position') | ||
| .then(res => res.json()) | ||
| .then(positions => { | ||
| if (mounted) setTrainPosition(positions.cvsr || null); | ||
| }) | ||
| .catch(() => { | ||
| if (mounted) setTrainPosition(null); | ||
| }); | ||
| }; | ||
|
|
||
| fetchPosition(); | ||
| const interval = setInterval(fetchPosition, POLL_INTERVAL_MS); | ||
|
|
||
| return () => { | ||
| mounted = false; | ||
| clearInterval(interval); | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
The useTrainPosition hook currently polls the /api/train/position endpoint unconditionally every 5 seconds for all users, even when the CVSR feature flag is disabled. This creates unnecessary network traffic and server load for 99%+ of users before the feature is publicly launched. We should pass an enabled parameter to the hook and only start polling if it is true.
export default function useTrainPosition(enabled = true) {
const [trainPosition, setTrainPosition] = useState(null);
useEffect(() => {
if (!enabled) {
setTrainPosition(null);
return;
}
let mounted = true;
const fetchPosition = () => {
fetch('/api/train/position')
.then(res => res.json())
.then(positions => {
if (mounted) setTrainPosition(positions.cvsr || null);
})
.catch(() => {
if (mounted) setTrainPosition(null);
});
};
fetchPosition();
const interval = setInterval(fetchPosition, POLL_INTERVAL_MS);
return () => {
mounted = false;
clearInterval(interval);
};
}, [enabled]);| const [showWaterTaxis, setShowWaterTaxis] = useState(true); | ||
| const cvsrEnabled = useMemo(() => isFeatureEnabled('CVSR'), []); | ||
| const boatPosition = useBoatPosition(); | ||
| const trainPosition = useTrainPosition(); |
| const snappedTrain = useMemo(() => { | ||
| if (!trainPosition || !trainFeature?.geometry?.coordinates) return null; | ||
| return snapToLine( | ||
| [trainPosition.latitude, trainPosition.longitude], | ||
| trainFeature.geometry.coordinates | ||
| ); | ||
| }, [trainPosition, trainFeature]); |
There was a problem hiding this comment.
If the API returns invalid or malformed coordinates (e.g., NaN or undefined), passing them to Leaflet's <Marker> component will cause a React render crash. We should defensively verify that both latitude and longitude are valid numbers before attempting to snap and render the marker.
| const snappedTrain = useMemo(() => { | |
| if (!trainPosition || !trainFeature?.geometry?.coordinates) return null; | |
| return snapToLine( | |
| [trainPosition.latitude, trainPosition.longitude], | |
| trainFeature.geometry.coordinates | |
| ); | |
| }, [trainPosition, trainFeature]); | |
| const snappedTrain = useMemo(() => { | |
| if (!trainPosition || typeof trainPosition.latitude !== 'number' || typeof trainPosition.longitude !== 'number' || !trainFeature?.geometry?.coordinates) return null; | |
| return snapToLine( | |
| [trainPosition.latitude, trainPosition.longitude], | |
| trainFeature.geometry.coordinates | |
| ); | |
| }, [trainPosition, trainFeature]); |
| async function pollDevices() { | ||
| try { | ||
| const res = await fetch(`${USFT_API_URL}/map/devices`, { | ||
| headers: { | ||
| 'Authorization': `Bearer ${jwt}`, | ||
| 'User-Agent': 'RootsOfTheValley/1.0 (+https://rootsofthevalley.org)', | ||
| }, | ||
| }); | ||
|
|
||
| if (res.status === 401) { | ||
| console.log('[TrainTracker] JWT expired, re-authenticating'); | ||
| await authenticate(); | ||
| return; | ||
| } | ||
|
|
||
| if (!res.ok) { | ||
| console.warn(`[TrainTracker] Poll failed: ${res.status}`); | ||
| return; | ||
| } | ||
|
|
||
| const devices = await res.json(); | ||
| if (!Array.isArray(devices) || devices.length === 0) return; | ||
|
|
||
| const device = devices[0]; | ||
| const loc = device.location; | ||
| if (!loc) return; | ||
|
|
||
| const lat = parseFloat(loc.latitude); | ||
| const lng = parseFloat(loc.longitude); | ||
| if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; | ||
|
|
||
| position = { | ||
| latitude: lat, | ||
| longitude: lng, | ||
| heading: parseInt(loc.heading, 10) || 0, | ||
| speed: parseInt(loc.velocity, 10) || 0, | ||
| status: deriveStatus(device), | ||
| updatedAt: loc.lastUpdated || new Date().toISOString(), | ||
| }; | ||
| } catch (err) { | ||
| console.warn(`[TrainTracker] Poll error: ${err.message}`); | ||
| } | ||
| } |
There was a problem hiding this comment.
Using setInterval for asynchronous polling can lead to concurrent executions of pollDevices if a request hangs or takes longer than POLL_INTERVAL_MS. This can cause a race condition or multiple concurrent authentication requests to the USFT API. We should use a boolean flag to prevent concurrent runs. Additionally, we should defensively check if device is valid before accessing its properties.
let isPolling = false;
async function pollDevices() {
if (isPolling) return;
isPolling = true;
try {
const res = await fetch(`${USFT_API_URL}/map/devices`, {
headers: {
'Authorization': `Bearer ${jwt}`,
'User-Agent': 'RootsOfTheValley/1.0 (+https://rootsofthevalley.org)',
},
});
if (res.status === 401) {
console.log('[TrainTracker] JWT expired, re-authenticating');
await authenticate();
return;
}
if (!res.ok) {
console.warn(`[TrainTracker] Poll failed: ${res.status}`);
return;
}
const devices = await res.json();
if (!Array.isArray(devices) || devices.length === 0) return;
const device = devices[0];
if (!device) return;
const loc = device.location;
if (!loc) return;
const lat = parseFloat(loc.latitude);
const lng = parseFloat(loc.longitude);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
position = {
latitude: lat,
longitude: lng,
heading: parseInt(loc.heading, 10) || 0,
speed: parseInt(loc.velocity, 10) || 0,
status: deriveStatus(device),
updatedAt: loc.lastUpdated || new Date().toISOString(),
};
} catch (err) {
console.warn(`[TrainTracker] Poll error: ${err.message}`);
} finally {
isPolling = false;
}
}| await authenticate(); | ||
| startPolling(); |
There was a problem hiding this comment.
If the USFT API is temporarily down or unreachable when the backend server starts up, authenticate() will throw an error, preventing startPolling() from ever being called. This means the train tracker will remain completely dead until the entire backend server is restarted. We should catch any initial authentication error and still start polling, allowing the service to automatically recover and authenticate on subsequent poll attempts.
try {
await authenticate();
} catch (err) {
console.warn('[TrainTracker] Initial USFT authentication failed, will retry during polling:', err.message);
}
startPolling();| const age = Date.now() - new Date(position.updatedAt).getTime(); | ||
| const threshold = position.status === 'active' ? ACTIVE_STALE_MS : IDLE_STALE_MS; | ||
| if (age > threshold) { | ||
| return { cvsr: null }; | ||
| } |
There was a problem hiding this comment.
If position.updatedAt is an invalid date string, new Date(position.updatedAt).getTime() will return NaN. In JavaScript, NaN > threshold is always false, which means the stale check will be bypassed and stale data will be served indefinitely. We should parse the date safely and handle NaN by defaulting to Infinity age.
const parsedDate = Date.parse(position.updatedAt);
const age = Number.isNaN(parsedDate) ? Infinity : Date.now() - parsedDate;
const threshold = position.status === 'active' ? ACTIVE_STALE_MS : IDLE_STALE_MS;
if (age > threshold) {
return { cvsr: null };
}| fetch('/api/train/position').then(r => r.json()).then(data => { | ||
| const pos = data?.cvsr; | ||
| if (pos) { | ||
| const pad = 0.005; | ||
| setBoundsToFit([[pos.latitude - pad, pos.longitude - pad], | ||
| [pos.latitude + pad, pos.longitude + pad]]); | ||
| setFitNonce(n => n + 1); | ||
| } |
There was a problem hiding this comment.
If the API returns invalid or missing coordinates for the train position, pos.latitude - pad will evaluate to NaN. Passing NaN bounds to Leaflet's setBoundsToFit can cause Leaflet to crash or break map rendering. We should defensively verify that both latitude and longitude are valid numbers before setting the bounds.
| fetch('/api/train/position').then(r => r.json()).then(data => { | |
| const pos = data?.cvsr; | |
| if (pos) { | |
| const pad = 0.005; | |
| setBoundsToFit([[pos.latitude - pad, pos.longitude - pad], | |
| [pos.latitude + pad, pos.longitude + pad]]); | |
| setFitNonce(n => n + 1); | |
| } | |
| fetch('/api/train/position').then(r => r.json()).then(data => { | |
| const pos = data?.cvsr; | |
| if (pos && typeof pos.latitude === 'number' && typeof pos.longitude === 'number') { | |
| const pad = 0.005; | |
| setBoundsToFit([[pos.latitude - pad, pos.longitude - pad], | |
| [pos.latitude + pad, pos.longitude + pad]]); | |
| setFitNonce(n => n + 1); | |
| } |
| export function snapToLine(point, lineCoords) { | ||
| if (!lineCoords || lineCoords.length < 2) return { position: point, bearing: 0 }; |
There was a problem hiding this comment.
We should defensively check that the input point is a valid array with two numeric coordinates before performing calculations to prevent runtime errors.
| export function snapToLine(point, lineCoords) { | |
| if (!lineCoords || lineCoords.length < 2) return { position: point, bearing: 0 }; | |
| export function snapToLine(point, lineCoords) { | |
| if (!point || !Array.isArray(point) || point.length < 2 || typeof point[0] !== 'number' || typeof point[1] !== 'number') { | |
| return { position: point, bearing: 0 }; | |
| } | |
| if (!lineCoords || lineCoords.length < 2) return { position: point, bearing: 0 }; |
| export function isFeatureEnabled(flag) { | ||
| const params = new URLSearchParams(window.location.search); | ||
| if (params.get('feature') === flag) { | ||
| if (params.get('off') === 'true') { | ||
| localStorage.removeItem(`feature_${flag}`); | ||
| return false; | ||
| } | ||
| localStorage.setItem(`feature_${flag}`, 'true'); | ||
| return true; | ||
| } | ||
| return localStorage.getItem(`feature_${flag}`) === 'true'; | ||
| } |
There was a problem hiding this comment.
If a user has blocked local storage or is using a strict private browsing mode, calling localStorage.setItem or localStorage.getItem will throw a SecurityError and crash the entire application on load. We should wrap the storage access in a try-catch block to ensure the application remains functional.
| export function isFeatureEnabled(flag) { | |
| const params = new URLSearchParams(window.location.search); | |
| if (params.get('feature') === flag) { | |
| if (params.get('off') === 'true') { | |
| localStorage.removeItem(`feature_${flag}`); | |
| return false; | |
| } | |
| localStorage.setItem(`feature_${flag}`, 'true'); | |
| return true; | |
| } | |
| return localStorage.getItem(`feature_${flag}`) === 'true'; | |
| } | |
| export function isFeatureEnabled(flag) { | |
| try { | |
| const params = new URLSearchParams(window.location.search); | |
| if (params.get('feature') === flag) { | |
| if (params.get('off') === 'true') { | |
| localStorage.removeItem(`feature_${flag}`); | |
| return false; | |
| } | |
| localStorage.setItem(`feature_${flag}`, 'true'); | |
| return true; | |
| } | |
| return localStorage.getItem(`feature_${flag}`) === 'true'; | |
| } catch (e) { | |
| const params = new URLSearchParams(window.location.search); | |
| return params.get('feature') === flag; | |
| } | |
| } |
Summary
trainTrackerService.js) polls US Fleet Tracking API every 5s with JWT auth?feature=CVSRURL flag gates visibility for partner demos before public launch/cuyahoga-valley-scenic-railroadzooms to train's GPS position and opens sidebarrailroadas a new linear feature type with brown dashed stylingCloses #529
Environment Variables (production)
USFT_SHARING_TOKEN— required, the USFT shared-view token from Alan at CVSRUSFT_API_URL— optional, defaults tohttps://hades.usft.comUSFT_POLL_INTERVAL_MS— optional, defaults to5000Test plan
?feature=CVSR— track line and train icon appear/cuyahoga-valley-scenic-railroad— zooms to train position, sidebar openscurl /api/train/position— returns JSON with cvsr position?feature=CVSR&off=true— clears flag, train disappears🤖 Generated with Claude Code