From a592d3de28ae824a4fe943dc7b8e59fb69168a81 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 30 Apr 2026 05:21:00 -0700 Subject: [PATCH 1/4] fix: timeline selections starting at offset 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dragging on the timeline ruler from the very start (offset 0) failed to create a section. The same falsy-zero pattern affected click-route URLs, where with the URL fix in place the click would otherwise produce /log_id/0/N instead of the canonical /log_id. - TIMELINE_PUSH_SELECTION reducer: replace !action.start with action.start == null so a drag at second 0 stops getting treated as "no selection" (which previously cleared zoom and the loop). - urlForState: drop start/end only when not finite or when start === end (was: dropped whenever start === 0). - DriveListItem: dispatch null/null on click instead of (0, duration) so the URL stays /dongleId/log_id — refreshing on a still-uploading route picks up the latest duration instead of pinning the old end. - updateTimeline: when start/end aren't finite, default the loop to (0, route.duration) by looking up the route in state.routes. - Update the unit test that was asserting the buggy URL. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/actions/index.js | 14 ++++++++++---- src/actions/index.test.js | 2 +- src/components/Dashboard/DriveListItem.jsx | 2 +- src/reducers/globalState.js | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index c523161d..b6e1e1a4 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -148,7 +148,7 @@ export function urlForState(dongleId, log_id, start, end, prime) { if (log_id) { path.push(log_id); - if (start && end && start > 0) { + if (Number.isFinite(start) && Number.isFinite(end) && start !== end) { path.push(start); path.push(end); } @@ -160,10 +160,16 @@ export function urlForState(dongleId, log_id, start, end, prime) { } function updateTimeline(state, dispatch, log_id, start, end, allowPathChange) { - if (!state.loop || !state.loop.startTime || !state.loop.duration || state.loop.startTime < start - || state.loop.startTime + state.loop.duration > end || state.loop.duration < end - start) { + // null/null = "loop the full route" (URL stays /dongleId/log_id so a refresh + // on a still-uploading route picks up the latest duration). + const loopStart = Number.isFinite(start) ? start : 0; + const loopEnd = Number.isFinite(end) ? end : state.routes?.find((r) => r.log_id === log_id)?.duration; + + if (Number.isFinite(loopEnd) && (!state.loop || !state.loop.duration + || state.loop.startTime < loopStart || state.loop.startTime + state.loop.duration > loopEnd + || state.loop.duration < loopEnd - loopStart)) { dispatch(resetPlayback()); - dispatch(selectLoop(start, end)); + dispatch(selectLoop(loopStart, loopEnd)); } if (allowPathChange) { diff --git a/src/actions/index.test.js b/src/actions/index.test.js index 133accf5..8962fcfb 100644 --- a/src/actions/index.test.js +++ b/src/actions/index.test.js @@ -23,6 +23,6 @@ describe('timeline actions', () => { zoom: {}, })); actionThunk(dispatch, getState); - expect(push).toBeCalledWith('/statedongle/log_id'); + expect(push).toBeCalledWith('/statedongle/log_id/0/1'); }); }); \ No newline at end of file diff --git a/src/components/Dashboard/DriveListItem.jsx b/src/components/Dashboard/DriveListItem.jsx index 00f42cca..aa4204f7 100644 --- a/src/components/Dashboard/DriveListItem.jsx +++ b/src/components/Dashboard/DriveListItem.jsx @@ -84,7 +84,7 @@ const DriveListItem = (props) => { }, [drive, dispatch, isVisible, el]); const onClick = filterRegularClick( - () => dispatch(pushTimelineRange(drive.log_id, 0, drive.duration, true)), + () => dispatch(pushTimelineRange(drive.log_id, null, null, true)), ); const small = windowWidth < 580; diff --git a/src/reducers/globalState.js b/src/reducers/globalState.js index e0a394c3..1f31f36f 100644 --- a/src/reducers/globalState.js +++ b/src/reducers/globalState.js @@ -289,7 +289,7 @@ export default function reducer(_state, action) { } break; case Types.TIMELINE_PUSH_SELECTION: { - if (!state.zoom || !action.start || !action.end || action.start < state.zoom.start || action.end > state.zoom.end) { + if (!state.zoom || action.start == null || action.end == null || action.start < state.zoom.start || action.end > state.zoom.end) { state.files = null; } @@ -300,7 +300,7 @@ export default function reducer(_state, action) { const r = state.routes?.find((route) => route.log_id === action.log_id); if (action.log_id && r) { state.currentRoute = r; - if (!action.start) { + if (action.start == null) { state.zoom = { start: 0, end: state.currentRoute.duration, From 1f402dc2c3636e729d9fe156e9ec21b3aa033fc3 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 4 May 2026 15:21:04 -0700 Subject: [PATCH 2/4] simplify with ?? and != null --- src/actions/index.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index b6e1e1a4..a4f0f5d9 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -148,7 +148,7 @@ export function urlForState(dongleId, log_id, start, end, prime) { if (log_id) { path.push(log_id); - if (Number.isFinite(start) && Number.isFinite(end) && start !== end) { + if (start != null && end != null && start !== end) { path.push(start); path.push(end); } @@ -160,12 +160,10 @@ export function urlForState(dongleId, log_id, start, end, prime) { } function updateTimeline(state, dispatch, log_id, start, end, allowPathChange) { - // null/null = "loop the full route" (URL stays /dongleId/log_id so a refresh - // on a still-uploading route picks up the latest duration). - const loopStart = Number.isFinite(start) ? start : 0; - const loopEnd = Number.isFinite(end) ? end : state.routes?.find((r) => r.log_id === log_id)?.duration; + const loopStart = start ?? 0; + const loopEnd = end ?? state.routes?.find((r) => r.log_id === log_id)?.duration; - if (Number.isFinite(loopEnd) && (!state.loop || !state.loop.duration + if (loopEnd != null && (!state.loop || !state.loop.duration || state.loop.startTime < loopStart || state.loop.startTime + state.loop.duration > loopEnd || state.loop.duration < loopEnd - loopStart)) { dispatch(resetPlayback()); From fde5416744c0a75746357722ee87c7d20dcd4acf Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 4 May 2026 15:59:48 -0700 Subject: [PATCH 3/4] simple --- src/actions/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/actions/index.js b/src/actions/index.js index a4f0f5d9..9d5816b1 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -160,8 +160,9 @@ export function urlForState(dongleId, log_id, start, end, prime) { } function updateTimeline(state, dispatch, log_id, start, end, allowPathChange) { + const route = state.routes?.find((r) => r.log_id === log_id); const loopStart = start ?? 0; - const loopEnd = end ?? state.routes?.find((r) => r.log_id === log_id)?.duration; + const loopEnd = end ?? route?.duration; if (loopEnd != null && (!state.loop || !state.loop.duration || state.loop.startTime < loopStart || state.loop.startTime + state.loop.duration > loopEnd @@ -171,7 +172,10 @@ function updateTimeline(state, dispatch, log_id, start, end, allowPathChange) { } if (allowPathChange) { - const desiredPath = urlForState(state.dongleId, log_id, Math.floor(start/1000), Math.floor(end/1000), false); + const wholeRoute = !Number.isFinite(start) || !Number.isFinite(end) || (start === 0 && end === route?.duration); + const urlStart = wholeRoute ? null : Math.floor(start / 1000); + const urlEnd = wholeRoute ? null : Math.floor(end / 1000); + const desiredPath = urlForState(state.dongleId, log_id, urlStart, urlEnd, false); if (window.location.pathname !== desiredPath) { dispatch(push(desiredPath)); } From fa0e56d11650fdd39704974a29958ed664e24cde Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 4 May 2026 16:03:15 -0700 Subject: [PATCH 4/4] sanitize NaN at the action boundary so state never holds garbage --- src/actions/index.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/actions/index.js b/src/actions/index.js index 9d5816b1..ad220a16 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -172,7 +172,7 @@ function updateTimeline(state, dispatch, log_id, start, end, allowPathChange) { } if (allowPathChange) { - const wholeRoute = !Number.isFinite(start) || !Number.isFinite(end) || (start === 0 && end === route?.duration); + const wholeRoute = start == null || (start === 0 && end === route?.duration); const urlStart = wholeRoute ? null : Math.floor(start / 1000); const urlEnd = wholeRoute ? null : Math.floor(end / 1000); const desiredPath = urlForState(state.dongleId, log_id, urlStart, urlEnd, false); @@ -197,6 +197,8 @@ export function popTimelineRange(log_id, allowPathChange = true) { } export function pushTimelineRange(log_id, start, end, allowPathChange = true) { + if (!Number.isFinite(start)) start = null; + if (!Number.isFinite(end)) end = null; return (dispatch, getState) => { const state = getState();