diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css index 318faa6ed..324d36a3d 100644 --- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css +++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes.css @@ -1254,7 +1254,8 @@ } .dashcam-video-shell { - aspect-ratio: 16 / 9; + /* Match qcamera.ts so the visible frame does not widen during the full-quality swap. */ + aspect-ratio: 526 / 330; background: #07080b; overflow: hidden; position: relative; diff --git a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js index 6262ad80f..e52272a9a 100644 --- a/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js +++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js @@ -6,6 +6,16 @@ function validDate(value) { return Number.isNaN(date.getTime()) ? null : date } +export function normalizeRouteSearchText(value) { + return String(value || "") + .normalize("NFKD") + .replace(/\p{M}/gu, "") + .toLocaleLowerCase() + .replace(/(\d+)(?:st|nd|rd|th)\b/g, "$1") + .replace(/[^\p{L}\p{N}]+/gu, " ") + .trim() +} + export function formatRouteDate(value, locale) { const date = validDate(value) if (!date) return "Unknown date" @@ -19,8 +29,17 @@ export function normalizeRoute(route, locale) { const timestamp = route?.timestamp == null ? null : String(route.timestamp) const startedAtDate = validDate(route?.startedAt) const timestampDate = validDate(timestamp) - const displayDate = formatRouteDate(startedAtDate || timestampDate, locale) + const routeDate = startedAtDate || timestampDate + const displayDate = formatRouteDate(routeDate, locale) const isCustomName = Boolean(route?.isCustomName) || Boolean(timestamp && !timestampDate) + const displayName = isCustomName ? timestamp : displayDate + const dateAliases = routeDate ? [ + new Intl.DateTimeFormat(locale, { dateStyle: "long" }).format(routeDate), + new Intl.DateTimeFormat("en-US", { month: "long", day: "numeric", year: "numeric" }).format(routeDate), + new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric" }).format(routeDate), + `${routeDate.getMonth() + 1}/${routeDate.getDate()}/${routeDate.getFullYear()}`, + `${routeDate.getFullYear()}-${routeDate.getMonth() + 1}-${routeDate.getDate()}`, + ] : [] return { ...route, @@ -29,8 +48,13 @@ export function normalizeRoute(route, locale) { startedAt: route?.startedAt || null, isCustomName, displayDate, - displayName: isCustomName ? timestamp : displayDate, + displayName, _startedAtMs: startedAtDate?.getTime() ?? timestampDate?.getTime() ?? null, + _searchValues: [ + normalizeRouteSearchText(route?.name), + normalizeRouteSearchText(isCustomName ? displayName : ""), + ...dateAliases.map(normalizeRouteSearchText), + ].filter(Boolean), } } @@ -96,11 +120,19 @@ export function sortRoutes(routes, sortOrder = "newest") { } export function routeMatchesSearch(route, searchQuery) { - const query = String(searchQuery || "").trim().toLocaleLowerCase() - if (!query) return true - return [route?.name, route?.timestamp, route?.displayName, route?.displayDate] - .filter(Boolean) - .some(value => String(value).toLocaleLowerCase().includes(query)) + const queryTokens = normalizeRouteSearchText(searchQuery).split(" ").filter(Boolean) + if (!queryTokens.length) return true + + const searchValues = Array.isArray(route?._searchValues) ? route._searchValues : [ + route?.name, + route?.timestamp, + route?.displayName, + route?.displayDate, + ].map(normalizeRouteSearchText).filter(Boolean) + return searchValues.some(value => { + const searchTokens = value.split(" ").filter(Boolean) + return queryTokens.every(queryToken => searchTokens.some(searchToken => searchToken.includes(queryToken))) + }) } export function buildRouteView(routes, options = {}) { diff --git a/starpilot/system/the_galaxy/tests/test_dashcam_routes_helpers.py b/starpilot/system/the_galaxy/tests/test_dashcam_routes_helpers.py index 2c7007570..a52963950 100644 --- a/starpilot/system/the_galaxy/tests/test_dashcam_routes_helpers.py +++ b/starpilot/system/the_galaxy/tests/test_dashcam_routes_helpers.py @@ -15,6 +15,7 @@ import pytest HELPERS_PATH = Path(__file__).resolve().parent.parent / "assets" / "components" / "recordings" / "dashcam_routes_helpers.js" COMPONENT_PATH = HELPERS_PATH.with_name("dashcam_routes.js") +COMPONENT_CSS_PATH = HELPERS_PATH.with_name("dashcam_routes.css") # node infers ESM from `export` syntax in a bare .js file from 22.7 on, so the helpers # need no package.json and stay a normal asset next to the component that imports them. @@ -78,6 +79,12 @@ def test_route_titles_and_custom_name_badges_are_reactive(): assert source.count('${() => route.isCustomName ? html`') >= 4 +def test_player_shell_matches_low_quality_video_aspect_ratio(): + source = COMPONENT_CSS_PATH.read_text(encoding="utf-8") + + assert "aspect-ratio: 526 / 330;" in source + + def test_groups_routes_into_today_yesterday_dates_and_unknown(): groups = evaluate(''' const routes = [ @@ -129,6 +136,33 @@ def test_searches_custom_names_displayed_dates_and_route_ids(): assert matches == [1, 1, 1, 0] +def test_search_matches_partial_title_tokens_and_friendly_dates_as_the_user_types(): + result = evaluate(''' + const routes = [ + route("0000006a--9f0a7bdf9c", "2026-08-27T08:00:00Z", { timestamp: "Test_31", isCustomName: true }), + route("0000006b--9f0a7bdf9d", "2026-08-28T08:00:00Z", { timestamp: "Morning drive", isCustomName: true }), + route("0000006c--9f0a7bdf9e", "2026-09-27T08:00:00Z", { timestamp: "Test_4", isCustomName: true }), + ] + return ["test", "test 3", "aug", "aug 2", "aug 27th", "8/27"] + .map(searchQuery => buildRouteView(routes, { searchQuery }).matching.map(item => item.name)) + ''') + + assert result == [ + ["0000006c--9f0a7bdf9e", "0000006a--9f0a7bdf9c"], + ["0000006a--9f0a7bdf9c"], + ["0000006b--9f0a7bdf9d", "0000006a--9f0a7bdf9c"], + ["0000006b--9f0a7bdf9d", "0000006a--9f0a7bdf9c"], + ["0000006a--9f0a7bdf9c"], + ["0000006a--9f0a7bdf9c"], + ] + + +def test_route_search_input_updates_on_every_keystroke(): + source = COMPONENT_PATH.read_text(encoding="utf-8") + + assert '@input="${event => { state.searchQuery = event.target.value }}"' in source + + def test_filters_preserved_routes_before_applying_the_render_limit(): view = evaluate(''' const routes = Array.from({ length: MAX_RENDERED_ROUTES + 25 }, (_, index) => route(