Skip to content
This repository was archived by the owner on Jun 24, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/App.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ describe('Demo mode', () => {

test('View demo route', async () => {
const { findByText, findByTestId } = renderApp(`/${Demo.DONGLE_ID}/${DEMO_LOG_ID}`)
expect(await findByText(DEMO_LOG_ID)).toBeTruthy()
// Route ID includes the current segment, which starts at 0
expect(await findByText(`${DEMO_LOG_ID}/0`)).toBeTruthy()
const video = (await findByTestId('route-video')) as HTMLVideoElement
await waitFor(() => expect(video.src).toBeTruthy())
})
Expand All @@ -42,7 +43,7 @@ describe.skip('Public routes', () => {

test('View public route without signing in', async () => {
const { findByText } = renderApp(`/${Demo.DONGLE_ID}/${DEMO_LOG_ID}`)
expect(await findByText(DEMO_LOG_ID)).toBeTruthy()
expect(await findByText(`${DEMO_LOG_ID}/0`)).toBeTruthy()
// Videos do not load, yet
// const video = (await findByTestId('route-video')) as HTMLVideoElement
// await waitFor(() => expect(video.src).toBeTruthy())
Expand Down
15 changes: 11 additions & 4 deletions src/components/RouteActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { USERADMIN_URL } from '~/api/config'
import { setRoutePublic, setRoutePreserved, getPreservedRoutes, parseRouteName } from '~/api/route'
import Icon from '~/components/material/Icon'
import type { Route } from '~/api/types'
import { getRouteSegment } from '~/utils/format'

const ToggleButton: VoidComponent<{
label: string
Expand Down Expand Up @@ -35,6 +36,7 @@ const ToggleButton: VoidComponent<{
interface RouteActionsProps {
routeName: string
route: Route | undefined
seekTime: number
}

const RouteActions: VoidComponent<RouteActionsProps> = (props) => {
Expand All @@ -43,7 +45,10 @@ const RouteActions: VoidComponent<RouteActionsProps> = (props) => {
const [isPublic, setIsPublic] = createSignal<boolean | undefined>(undefined)
const [isPreserved, setIsPreserved] = createSignal<boolean | undefined>(undefined)

const useradminUrl = () => `${USERADMIN_URL}/?onebox=${currentRouteId()}`
const routeInfo = () => parseRouteName(props.routeName)
const segment = () => getRouteSegment(props.seekTime, props.route?.maxqlog)

const useradminUrl = () => `${USERADMIN_URL}/?onebox=${routeInfo().dongleId}/${routeInfo().routeId}`

createEffect(() => {
const preservedRoutes = preservedRoutesResource()
Expand Down Expand Up @@ -88,7 +93,7 @@ const RouteActions: VoidComponent<RouteActionsProps> = (props) => {
}
}

const currentRouteId = () => props.routeName.replace('|', '/')
const currentRouteId = () => `${routeInfo().dongleId}/${routeInfo().routeId}/${segment()}`

const copyCurrentRouteId = async () => {
if (!props.routeName || !navigator.clipboard) return
Expand Down Expand Up @@ -116,8 +121,10 @@ const RouteActions: VoidComponent<RouteActionsProps> = (props) => {
class="flex w-full cursor-pointer items-center justify-between rounded-lg border-2 border-surface-container-high bg-surface-container-lowest p-3 hover:bg-surface-container-low"
>
<div class="lg:text-sm">
<span class="break-keep inline-block">{currentRouteId().split('/')[0] || ''}/</span>
<span class="break-keep inline-block">{currentRouteId().split('/')[1] || ''}</span>
<span class="break-keep inline-block">{routeInfo().dongleId}/</span>
<span class="break-keep inline-block">
{routeInfo().routeId}/{segment()}
</span>
</div>
<Icon class={clsx('mx-2', copied() && 'text-green-300')} name={copied() ? 'check' : 'file_copy'} size="20" />
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/dashboard/activities/RouteActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const RouteActivity: VoidComponent<RouteActivityProps> = (props) => {
<div class="flex flex-col rounded-md overflow-hidden bg-surface-container">
<RouteStatisticsBar class="p-5" route={route()} statistics={statistics} />

<RouteActions routeName={routeName()} route={route()} />
<RouteActions routeName={routeName()} route={route()} seekTime={seekTime()} />
</div>
</div>

Expand Down
19 changes: 18 additions & 1 deletion src/utils/format.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'

import { dateTimeToColorBetween, formatDate, formatDistance, formatDuration } from './format'
import { dateTimeToColorBetween, formatDate, formatDistance, formatDuration, getRouteSegment } from './format'

describe('formatDistance', () => {
it('should format distance', () => {
Expand All @@ -25,6 +25,23 @@ describe('formatDuration', () => {
})
})

describe('getRouteSegment', () => {
it('should map a playback time to its segment number', () => {
expect(getRouteSegment(0)).toBe(0)
expect(getRouteSegment(59.9)).toBe(0)
expect(getRouteSegment(60)).toBe(1)
expect(getRouteSegment(125)).toBe(2)
})
it('should never return a negative segment', () => {
expect(getRouteSegment(-10)).toBe(0)
})
it('should clamp to the last available segment', () => {
expect(getRouteSegment(120, 5)).toBe(2)
expect(getRouteSegment(600, 3)).toBe(3)
expect(getRouteSegment(60, 0)).toBe(0)
})
})

describe('formatDate', () => {
beforeEach(() => {
vi.useFakeTimers()
Expand Down
10 changes: 10 additions & 0 deletions src/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ export const formatVideoTime = (seconds: number): string => {
return `${minutes}:${remainingSeconds.padStart(2, '0')}`
}

// openpilot splits every route into fixed-length segments
export const SEGMENT_LENGTH = 60 // seconds

// Map a playback time (in seconds) to its segment number, clamped to [0, maxSegment]
export const getRouteSegment = (seekTime: number, maxSegment?: number): number => {
const segment = Math.max(0, Math.floor(seekTime / SEGMENT_LENGTH))
if (maxSegment === undefined) return segment
return Math.min(segment, Math.max(0, maxSegment))
}

export const getRouteDuration = (route: Route | undefined): Duration | undefined => {
if (!route || !route.start_time || !route.end_time) return undefined
const startTime = dayjs(route.start_time)
Expand Down
Loading