-
Dashcam Routes
-
+
+
+ ${() => state.deleteMode ? Modal({
+ title: state.deleteMode === "all" ? "Delete All Routes, Including Preserved?" : "Delete All Non-Preserved Routes?",
+ message: state.deleteMode === "all"
+ ? "This permanently deletes every local route, including preserved routes. This action cannot be undone."
+ : "This permanently deletes every non-preserved local route. Preserved routes will be kept.",
+ onConfirm: () => deleteAllRoutes(state.deleteMode === "all"),
+ onCancel: () => { state.deleteMode = null },
+ confirmText: state.deleteMode === "all" ? "Delete Everything" : "Delete Non-Preserved",
+ }) : ""}
+
`
}
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
new file mode 100644
index 000000000..3ea1a613e
--- /dev/null
+++ b/starpilot/system/the_galaxy/assets/components/recordings/dashcam_routes_helpers.js
@@ -0,0 +1,284 @@
+export const MAX_RENDERED_ROUTES = 250
+const SEARCH_MONTHS = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"]
+
+function validDate(value) {
+ if (!value) return null
+ const date = value instanceof Date ? new Date(value.getTime()) : new Date(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"
+ return new Intl.DateTimeFormat(locale, {
+ dateStyle: "long",
+ timeStyle: "short",
+ }).format(date)
+}
+
+export function normalizeRoute(route, locale) {
+ const timestamp = route?.timestamp == null ? null : String(route.timestamp)
+ const startedAtDate = validDate(route?.startedAt)
+ const timestampDate = validDate(timestamp)
+ 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()}`,
+ ] : []
+ const timeAliases = routeDate ? [
+ new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit" }).format(routeDate),
+ new Intl.DateTimeFormat("en-US", { hour: "numeric", minute: "2-digit", hour12: true }).format(routeDate),
+ `${String(routeDate.getHours()).padStart(2, "0")}:${String(routeDate.getMinutes()).padStart(2, "0")}`,
+ ] : []
+
+ return {
+ ...route,
+ name: String(route?.name || ""),
+ timestamp,
+ startedAt: route?.startedAt || null,
+ isCustomName,
+ displayDate,
+ displayName,
+ _startedAtMs: startedAtDate?.getTime() ?? timestampDate?.getTime() ?? null,
+ _searchIndex: {
+ ids: [normalizeRouteSearchText(route?.name)].filter(Boolean),
+ titles: [normalizeRouteSearchText(isCustomName ? displayName : "")].filter(Boolean),
+ dates: dateAliases.map(normalizeRouteSearchText).filter(Boolean),
+ times: timeAliases.map(normalizeRouteSearchText).filter(Boolean),
+ },
+ }
+}
+
+export function formatTotalDuration(seconds) {
+ const totalMinutes = Math.max(0, Math.round(Number(seconds) / 60) || 0)
+ if (totalMinutes < 1) return "0 min"
+ if (totalMinutes < 60) return `${totalMinutes} min`
+ const hours = Math.floor(totalMinutes / 60)
+ const remaining = totalMinutes % 60
+ return remaining > 0 ? `${hours}h ${remaining}m` : `${hours}h`
+}
+
+export function computeRouteStats(routes = []) {
+ const list = Array.isArray(routes) ? routes : []
+ let totalDurationSeconds = 0
+ let preservedCount = 0
+ let totalSegments = 0
+
+ for (const route of list) {
+ if (route) {
+ if (Number.isFinite(route.approxDurationSeconds)) {
+ totalDurationSeconds += Math.max(0, route.approxDurationSeconds)
+ }
+ if (route.is_preserved) {
+ preservedCount += 1
+ }
+ if (Number.isFinite(route.segmentCount)) {
+ totalSegments += Math.max(0, route.segmentCount)
+ }
+ }
+ }
+
+ return {
+ count: list.length,
+ totalDurationSeconds,
+ formattedDuration: formatTotalDuration(totalDurationSeconds),
+ preservedCount,
+ totalSegments,
+ }
+}
+
+export function sortRoutes(routes, sortOrder = "newest") {
+ if (sortOrder === "longest" || sortOrder === "shortest") {
+ const direction = sortOrder === "longest" ? -1 : 1
+ return [...routes].sort((left, right) => {
+ const leftDur = Number.isFinite(left?.approxDurationSeconds) ? left.approxDurationSeconds : -1
+ const rightDur = Number.isFinite(right?.approxDurationSeconds) ? right.approxDurationSeconds : -1
+ if (leftDur !== rightDur) return (leftDur - rightDur) * direction
+ return String(left?.name || "").localeCompare(String(right?.name || ""))
+ })
+ }
+
+ const direction = sortOrder === "oldest" ? 1 : -1
+ return [...routes].sort((left, right) => {
+ const leftTime = left?._startedAtMs
+ const rightTime = right?._startedAtMs
+ if (leftTime == null && rightTime == null) return String(left?.name || "").localeCompare(String(right?.name || ""))
+ if (leftTime == null) return 1
+ if (rightTime == null) return -1
+ if (leftTime !== rightTime) return (leftTime - rightTime) * direction
+ return String(left?.name || "").localeCompare(String(right?.name || "")) * -direction
+ })
+}
+
+export function routeMatchesSearch(route, searchQuery) {
+ const rawQuery = String(searchQuery || "").trim()
+ const normalizedQuery = normalizeRouteSearchText(searchQuery)
+ const queryTokens = normalizedQuery.split(" ").filter(Boolean)
+ if (!queryTokens.length) return true
+
+ const fallbackIndex = {
+ ids: [route?.name],
+ titles: [route?.timestamp, route?.displayName],
+ dates: [route?.displayDate],
+ times: [route?.displayDate],
+ }
+ const searchIndex = route?._searchIndex || Object.fromEntries(
+ Object.entries(fallbackIndex).map(([key, values]) => [key, values.map(normalizeRouteSearchText).filter(Boolean)]),
+ )
+
+ const hasMonth = queryTokens.some(token => token.length >= 3 && SEARCH_MONTHS.some(month => month.startsWith(token)))
+ const isDateQuery = hasMonth || /\d\s*[/-]\s*\d/.test(rawQuery) || /\d+(?:st|nd|rd|th)\b/i.test(rawQuery) || /^\d{4}$/.test(normalizedQuery)
+ const isTimeQuery = /^\d{1,2}$/.test(normalizedQuery) || /\d\s*:\s*\d/.test(rawQuery) || queryTokens.some(token => token === "am" || token === "pm")
+ const compactQuery = normalizedQuery.replaceAll(" ", "")
+ const isHexQuery = /^[0-9a-f]+$/.test(compactQuery)
+ const isIdQuery = rawQuery.includes("--") || (isHexQuery && (
+ compactQuery.length >= 8 || (compactQuery.length >= 4 && /\d/.test(compactQuery) && /[a-f]/.test(compactQuery))
+ ))
+ const valuesFor = key => Array.isArray(searchIndex[key]) ? searchIndex[key] : []
+ const matchesValue = (value, dateValue = false) => {
+ if (value.includes(normalizedQuery)) return true
+ const searchTokens = value.split(" ").filter(Boolean)
+ return queryTokens.every(queryToken => searchTokens.some(searchToken => {
+ // In a date query, "20" is a day prefix, not a match for the year "2026".
+ if (dateValue && /^\d{1,2}$/.test(queryToken) && /^\d{4}$/.test(searchToken)) return false
+ return searchToken.startsWith(queryToken)
+ }))
+ }
+
+ return valuesFor("titles").some(value => matchesValue(value))
+ || (isDateQuery && valuesFor("dates").some(value => matchesValue(value, true)))
+ || (isTimeQuery && valuesFor("times").some(value => matchesValue(value)))
+ || (isIdQuery && valuesFor("ids").some(value => matchesValue(value)))
+}
+
+export function buildRouteView(routes, options = {}) {
+ const matching = sortRoutes(
+ routes.filter(route => (!options.preservedOnly || route.is_preserved) && routeMatchesSearch(route, options.searchQuery)),
+ options.sortOrder,
+ )
+ return {
+ matching,
+ visible: matching.slice(0, MAX_RENDERED_ROUTES),
+ truncated: matching.length > MAX_RENDERED_ROUTES,
+ }
+}
+
+export function routeViewRenderKey(routes, sortOrder = "newest", viewMode = "list") {
+ const routeNames = Array.isArray(routes) ? routes.map(route => String(route?.name || "")).join(",") : ""
+ return `${viewMode}:${sortOrder}:${routeNames}`
+}
+
+function localDayKey(date) {
+ return `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
+}
+
+export function groupRoutesByDate(routes, now = new Date(), locale) {
+ const today = validDate(now) || new Date()
+ today.setHours(0, 0, 0, 0)
+ const yesterday = new Date(today)
+ yesterday.setDate(yesterday.getDate() - 1)
+ const groups = []
+ const byKey = new Map()
+
+ for (const route of routes) {
+ const routeDate = route?._startedAtMs == null ? null : new Date(route._startedAtMs)
+ const key = routeDate ? localDayKey(routeDate) : "unknown"
+ let group = byKey.get(key)
+ if (!group) {
+ let label = "Unknown date"
+ if (routeDate) {
+ if (key === localDayKey(today)) label = "Today"
+ else if (key === localDayKey(yesterday)) label = "Yesterday"
+ else label = new Intl.DateTimeFormat(locale, { dateStyle: "long" }).format(routeDate)
+ }
+ group = { key, label, routes: [] }
+ byKey.set(key, group)
+ groups.push(group)
+ }
+ group.routes.push(route)
+ }
+ return groups
+}
+
+export function groupRoutesForView(routes, sortOrder = "newest", now, locale) {
+ // Date headers would scramble a duration sort, so show one flat group instead.
+ if (sortOrder === "longest" || sortOrder === "shortest") {
+ const list = Array.isArray(routes) ? [...routes] : []
+ if (!list.length) return []
+ return [{ key: sortOrder, label: sortOrder === "longest" ? "Longest first" : "Shortest first", routes: list }]
+ }
+ return groupRoutesByDate(routes, now, locale)
+}
+
+export function formatApproxDuration(seconds) {
+ const minutes = Math.max(0, Math.round(Number(seconds) / 60) || 0)
+ if (minutes < 1) return "Less than 1 min"
+ if (minutes < 60) return `About ${minutes} min`
+ const hours = Math.floor(minutes / 60)
+ const remaining = minutes % 60
+ return `About ${hours} hr${remaining ? ` ${remaining} min` : ""}`
+}
+
+export function parseStoredSegmentNumber(segmentUrl) {
+ const cleanPath = String(segmentUrl || "").split(/[?#]/, 1)[0]
+ const match = cleanPath.match(/--(\d+)\/?$/)
+ if (!match) return null
+ const value = Number(match[1])
+ return Number.isSafeInteger(value) ? value : null
+}
+
+export function getSegmentStatus(segmentUrls, playbackIndex) {
+ if (!Array.isArray(segmentUrls) || !Number.isInteger(playbackIndex) || playbackIndex < 0 || playbackIndex >= segmentUrls.length) return ""
+ const segmentNumber = parseStoredSegmentNumber(segmentUrls[playbackIndex])
+ if (segmentNumber == null) return ""
+ return `Segment ${segmentNumber}`
+}
+
+export function getSegmentOptions(segmentUrls) {
+ if (!Array.isArray(segmentUrls)) return []
+ return segmentUrls.map((_, index) => ({
+ index,
+ label: getSegmentStatus(segmentUrls, index) || `Clip ${index + 1}`,
+ }))
+}
+
+export function cameraVideoUrl(segmentUrl, camera, quality) {
+ const separator = String(segmentUrl).includes("?") ? "&" : "?"
+ const url = `${segmentUrl}${separator}camera=${encodeURIComponent(camera)}`
+ return quality ? `${url}&quality=${encodeURIComponent(quality)}` : url
+}
+
+export function routeMetadataErrorMessage(status, serverError) {
+ if (status === 404) {
+ return "This route is no longer available on this device. Its local video segments may have been deleted or moved."
+ }
+ const detail = String(serverError || "").trim()
+ return detail || `Could not load route details (${status}).`
+}
+
+// loggerd only writes qcamera.ts alongside the road camera.
+export function supportsLowQuality(camera) {
+ return camera === "forward"
+}
+
+// qcamera is 526x330. Only a positively taller frame proves the real stream is already
+// playing; an unknown height upgrades rather than stranding the viewer on the preview.
+export function shouldUpgradeFromHeight(height) {
+ return !(Number.isFinite(height) && height > 400)
+}
diff --git a/starpilot/system/the_galaxy/assets/js/utils.js b/starpilot/system/the_galaxy/assets/js/utils.js
index b0303bee3..b00d24aac 100644
--- a/starpilot/system/the_galaxy/assets/js/utils.js
+++ b/starpilot/system/the_galaxy/assets/js/utils.js
@@ -37,6 +37,20 @@ export function parseErrorLogToDate(filename) {
return new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}`)
}
+/**
+ * Escape a value for interpolation into an HTML string
+ * @param {unknown} value
+ * @returns {string}
+ */
+export function escapeHtml(value) {
+ return String(value ?? "")
+ .replaceAll("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """)
+ .replaceAll("'", "'")
+}
+
/**
* Capitalize the first character of a string
* @param {string} str
diff --git a/starpilot/system/the_galaxy/tests/test_dashboard_stats.py b/starpilot/system/the_galaxy/tests/test_dashboard_stats.py
index 95a94a738..4f5d21c3d 100644
--- a/starpilot/system/the_galaxy/tests/test_dashboard_stats.py
+++ b/starpilot/system/the_galaxy/tests/test_dashboard_stats.py
@@ -59,6 +59,8 @@ sys.modules.setdefault("openpilot.starpilot.assets.theme_manager", theme_manager
import utilities
+_REAL_COMMON_PARAMS_MODULE = sys.modules.get("openpilot.common.params")
+
for _module_name, _module in _INITIAL_MODULES.items():
if _module is None:
sys.modules.pop(_module_name, None)
@@ -86,6 +88,8 @@ def _simple_module(name, **attrs):
def _install_server_import_stubs():
+ if _REAL_COMMON_PARAMS_MODULE is not None:
+ sys.modules["openpilot.common.params"] = _REAL_COMMON_PARAMS_MODULE
sys.modules["openpilot.system.loggerd.config"] = loggerd_config
sys.modules["openpilot.system.loggerd.deleter"] = loggerd_deleter
sys.modules["openpilot.system.loggerd.uploader"] = loggerd_uploader
@@ -130,6 +134,14 @@ def _install_server_import_stubs():
)
sys.modules["openpilot.common.realtime"] = _simple_module("openpilot.common.realtime", DT_HW=0.01)
+ sys.modules["openpilot.common.swaglog"] = _simple_module(
+ "openpilot.common.swaglog",
+ cloudlog=SimpleNamespace(
+ error=lambda *args, **kwargs: None,
+ exception=lambda *args, **kwargs: None,
+ info=lambda *args, **kwargs: None,
+ ),
+ )
sys.modules["openpilot.common.time_helpers"] = _simple_module("openpilot.common.time_helpers", system_time_valid=lambda: True)
sys.modules["openpilot.system.hardware"] = _simple_module(
"openpilot.system.hardware",
@@ -149,6 +161,15 @@ def _install_server_import_stubs():
get_longitudinal_maneuver_support=lambda *args, **kwargs: {},
)
sys.modules["panda"] = _simple_module("panda", Panda=lambda *args, **kwargs: SimpleNamespace(can_send=lambda *send_args, **send_kwargs: None))
+ msgq_module = _simple_module("msgq")
+ msgq_visionipc = _simple_module(
+ "msgq.visionipc",
+ VisionIpcClient=lambda *args, **kwargs: SimpleNamespace(connect=lambda *connect_args: False),
+ VisionStreamType=SimpleNamespace(VISION_STREAM_DRIVER=0),
+ )
+ msgq_module.visionipc = msgq_visionipc
+ sys.modules["msgq"] = msgq_module
+ sys.modules["msgq.visionipc"] = msgq_visionipc
model_manager.is_builtin_model_key = lambda value: False
model_manager.model_key_aliases = lambda value: [value]
@@ -337,17 +358,16 @@ class FakeDashboardAnalyzerProcess:
def test_route_inventory_counts_segments_without_video_probing(monkeypatch):
- segments = [
- SimpleNamespace(route_name=SimpleNamespace(time_str="route-new")),
- SimpleNamespace(route_name=SimpleNamespace(time_str="route-new")),
- SimpleNamespace(route_name=SimpleNamespace(time_str="route-new")),
- SimpleNamespace(route_name=SimpleNamespace(time_str="route-old")),
- ]
+ def segment(time_str, segment_num):
+ return SimpleNamespace(route_name=SimpleNamespace(time_str=time_str), segment_num=segment_num)
+
+ # route-new has aged out of its first two segments, so it no longer starts at --0.
+ segments = [segment("route-new", 4), segment("route-new", 2), segment("route-new", 3), segment("route-old", 0)]
monkeypatch.setattr(utilities, "get_all_segment_names", lambda _path: segments)
- assert utilities.get_routes_with_segment_counts("/tmp/routes") == [
- ("route-old", 1),
- ("route-new", 3),
+ assert utilities.get_routes_with_segment_details("/tmp/routes") == [
+ ("route-old", {"segmentCount": 1, "firstSegmentNum": 0}),
+ ("route-new", {"segmentCount": 3, "firstSegmentNum": 2}),
]
@@ -1071,6 +1091,28 @@ def test_clear_dashboard_route_history_keeps_durable_records(tmp_path, monkeypat
assert stats["modelUsage"]["orion"]["drives"] == 3
+def test_clear_dashboard_route_history_can_retain_preserved_routes(tmp_path, monkeypatch):
+ monkeypatch.setattr(utilities, "DASHBOARD_PARAMS_DIR", tmp_path)
+ params = FakeParams({
+ utilities.DASHBOARD_PERSISTENT_STATS_PARAM: {
+ "routes": {
+ "0000006a--9f0a7bdf9c": {"date": "2026-06-15T08:00:00"},
+ "0000006b--9f0a7bdf9d": {"date": "2026-06-16T08:00:00"},
+ },
+ "ignoredRoutes": ["0000006a--9f0a7bdf9c", "0000006b--9f0a7bdf9d"],
+ "personalRecords": {"cleanDriveStreak": {"drives": 4}},
+ },
+ })
+
+ removed = utilities.clear_dashboard_route_history(params, retained_route_names={"0000006a--9f0a7bdf9c"})
+
+ assert removed == 1
+ stats = utilities._load_dashboard_persistent_stats(params)
+ assert list(stats["routes"]) == ["0000006a--9f0a7bdf9c"]
+ assert stats["ignoredRoutes"] == ["0000006a--9f0a7bdf9c"]
+ assert stats["personalRecords"]["cleanDriveStreak"]["drives"] == 4
+
+
def test_lightweight_routes_surface_recent_drives_without_log_analysis(monkeypatch):
utilities._invalidate_dashboard_cache()
now = utilities.datetime.now().replace(hour=12, minute=0, second=0, microsecond=0)
diff --git a/starpilot/system/the_galaxy/tests/test_dashcam_routes.py b/starpilot/system/the_galaxy/tests/test_dashcam_routes.py
new file mode 100644
index 000000000..13864037e
--- /dev/null
+++ b/starpilot/system/the_galaxy/tests/test_dashcam_routes.py
@@ -0,0 +1,854 @@
+from concurrent.futures import ThreadPoolExecutor
+from datetime import datetime, timezone
+import io
+import os
+from pathlib import Path
+import subprocess
+import threading
+import time
+
+import pytest
+
+from test_dashboard_stats import FakeParams, MODULE_DIR, _install_server_import_stubs
+
+
+def _load_server_module():
+ import importlib.util
+ import sys
+
+ _install_server_import_stubs()
+ spec = importlib.util.spec_from_file_location("dashcam_routes_server", MODULE_DIR / "the_galaxy.py")
+ module = importlib.util.module_from_spec(spec)
+ sys.modules["dashcam_routes_server"] = module
+ spec.loader.exec_module(module)
+ return module
+
+
+the_galaxy = _load_server_module()
+utilities = the_galaxy.utilities
+ROUTE_NAME = "0000006a--9f0a7bdf9c"
+
+
+def _make_segment(root, route_name=ROUTE_NAME, segment_num=0):
+ segment = root / f"{route_name}--{segment_num}"
+ segment.mkdir(parents=True)
+ return segment
+
+
+def _make_client(monkeypatch, root):
+ assert the_galaxy._import_galaxy_web_symbols()
+ monkeypatch.setattr(the_galaxy, "FOOTAGE_PATHS", [str(root) + "/"])
+ monkeypatch.setattr(the_galaxy, "params", FakeParams())
+ app = the_galaxy.Flask(
+ f"dashcam_routes_{time.monotonic_ns()}",
+ template_folder=str(MODULE_DIR / "templates"),
+ static_folder=str(MODULE_DIR / "assets"),
+ )
+ the_galaxy.setup(app)
+ return app.test_client()
+
+
+def test_process_route_is_metadata_only_and_retains_fields(monkeypatch, tmp_path):
+ segment = _make_segment(tmp_path, segment_num=3)
+ (segment / "qlog.zst").write_bytes(b"log")
+ (segment / "Morning school run").touch()
+ started_at = datetime(2026, 8, 26, 15, 30, tzinfo=timezone.utc)
+ monkeypatch.setattr(utilities, "get_route_start_time", lambda path: started_at)
+ monkeypatch.setattr(utilities, "has_preserve_attr", lambda path: True)
+ monkeypatch.setattr(utilities, "video_to_png", lambda *args: (_ for _ in ()).throw(AssertionError("preview generation must stay lazy")))
+
+ result = utilities.process_route(str(tmp_path), ROUTE_NAME, segment_count=4, first_segment_num=3)
+
+ assert result == {
+ "name": ROUTE_NAME,
+ "png": f"/thumbnails/{ROUTE_NAME}--3/preview.png",
+ "timestamp": "Morning school run",
+ "startedAt": "2026-08-26T15:30:00Z",
+ "isCustomName": True,
+ "is_preserved": True,
+ "segmentCount": 4,
+ "approxDurationSeconds": 240,
+ }
+
+
+def test_process_route_uses_display_timestamp_without_losing_started_at(monkeypatch, tmp_path):
+ _make_segment(tmp_path)
+ started_at = datetime(2026, 8, 26, 15, 30, tzinfo=timezone.utc)
+ monkeypatch.setattr(utilities, "get_route_start_time", lambda path: started_at)
+
+ result = utilities.process_route(str(tmp_path), ROUTE_NAME, segment_count=1)
+
+ assert result["timestamp"] == started_at.isoformat()
+ assert result["startedAt"] == "2026-08-26T15:30:00Z"
+ assert result["isCustomName"] is False
+
+
+def test_route_scan_deduplicates_using_footage_root_priority(monkeypatch):
+ first = "/priority/"
+ second = "/fallback/"
+ details = {
+ first: [(ROUTE_NAME, {"segmentCount": 2, "firstSegmentNum": 1})],
+ second: [
+ (ROUTE_NAME, {"segmentCount": 8, "firstSegmentNum": 0}),
+ ("0000006b--9f0a7bdf9d", {"segmentCount": 1, "firstSegmentNum": 4}),
+ ],
+ }
+ monkeypatch.setattr(utilities, "get_routes_with_segment_details", lambda path: details[path])
+
+ entries = the_galaxy._route_scan_entries([first, second])
+
+ assert entries == [
+ (first, ROUTE_NAME, 2, 1),
+ (second, "0000006b--9f0a7bdf9d", 1, 4),
+ ]
+
+
+def test_route_metadata_stream_batches_eight_with_progress_and_retained_fields():
+ entries = [
+ ("/routes/", f"{index:08x}--{index:010x}", index + 1, index % 3)
+ for index in range(18)
+ ]
+
+ def process(path, name, segment_count, first_segment_num):
+ return {
+ "name": name,
+ "png": f"/thumbnails/{name}--{first_segment_num}/preview.png",
+ "timestamp": f"Route {segment_count}",
+ "startedAt": "2026-08-26T15:30:00Z",
+ "isCustomName": True,
+ "is_preserved": False,
+ "segmentCount": segment_count,
+ "approxDurationSeconds": segment_count * 60,
+ }
+
+ events = list(the_galaxy._route_metadata_events(entries, "dongle", process))
+
+ assert events[0] == {"routes": [], "progress": 0, "total": 18, "connectDongleId": "dongle"}
+ assert [len(event["routes"]) for event in events[1:]] == [8, 8, 2]
+ assert [event["progress"] for event in events[1:]] == [8, 16, 18]
+ assert all(event["total"] == 18 for event in events)
+ results = [route for event in events[1:] for route in event["routes"]]
+ assert len(results) == 18
+ assert all({
+ "name", "png", "timestamp", "startedAt", "isCustomName",
+ "is_preserved", "segmentCount", "approxDurationSeconds",
+ } <= result.keys() for result in results)
+
+
+def test_route_metadata_stream_cancels_queued_work_when_closed():
+ entries = [("/routes/", f"{index:08x}--{index:010x}", 1, 0) for index in range(40)]
+ release = threading.Event()
+ started = []
+ lock = threading.Lock()
+
+ def process(path, name, segment_count, first_segment_num):
+ index = int(name.split("--", 1)[0], 16)
+ with lock:
+ started.append(index)
+ if index >= 8:
+ release.wait(timeout=2)
+ return {"name": name}
+
+ stream = the_galaxy._route_metadata_events(entries, process_route=process)
+ next(stream)
+ batch = next(stream)
+ assert len(batch["routes"]) == 8
+ stream.close()
+ release.set()
+ time.sleep(0.1)
+
+ # At most four already-running workers continue; the remaining queue is cancelled.
+ assert len(started) <= 12
+
+
+def test_thumbnail_path_validation_is_strict(tmp_path):
+ _make_segment(tmp_path)
+ valid = f"{ROUTE_NAME}--0/preview.png"
+
+ assert the_galaxy._resolve_route_thumbnail(valid, [tmp_path]) == tmp_path / f"{ROUTE_NAME}--0" / "preview.png"
+ for invalid in (
+ "../preview.png",
+ f"{ROUTE_NAME}--0/qcamera.ts",
+ f"{ROUTE_NAME}--0/subdir/preview.png",
+ f"{ROUTE_NAME}--nope/preview.png",
+ f"/{ROUTE_NAME}--0/preview.png",
+ f"{ROUTE_NAME}--0\\preview.png",
+ ):
+ assert the_galaxy._resolve_route_thumbnail(invalid, [tmp_path]) is None
+
+
+def test_thumbnail_path_validation_rejects_symlinks_outside_the_footage_root(tmp_path):
+ footage_root = tmp_path / "footage"
+ outside_segment = tmp_path / "outside"
+ footage_root.mkdir()
+ outside_segment.mkdir()
+ (footage_root / f"{ROUTE_NAME}--0").symlink_to(outside_segment, target_is_directory=True)
+
+ assert the_galaxy._resolve_route_thumbnail(f"{ROUTE_NAME}--0/preview.png", [footage_root]) is None
+
+
+def test_thumbnail_generation_is_lazy_and_reuses_completed_preview(monkeypatch, tmp_path):
+ segment = _make_segment(tmp_path)
+ (segment / "qcamera.ts").write_bytes(b"video")
+ calls = []
+
+ def generate(source, output):
+ calls.append((Path(source), Path(output)))
+ Path(output).write_bytes(b"png")
+ return True
+
+ monkeypatch.setattr(utilities, "video_to_png", generate)
+ relative_path = f"{ROUTE_NAME}--0/preview.png"
+
+ first = the_galaxy._get_or_create_route_thumbnail(relative_path, [tmp_path])
+ second = the_galaxy._get_or_create_route_thumbnail(relative_path, [tmp_path])
+
+ assert first == second == segment / "preview.png"
+ assert len(calls) == 1
+ assert calls[0][0] == segment / "qcamera.ts"
+
+
+def test_thumbnail_failure_returns_none_and_does_not_cache_partial_file(monkeypatch, tmp_path):
+ segment = _make_segment(tmp_path)
+ (segment / "qcamera.ts").write_bytes(b"video")
+ monkeypatch.setattr(utilities, "video_to_png", lambda source, output: False)
+
+ result = the_galaxy._get_or_create_route_thumbnail(f"{ROUTE_NAME}--0/preview.png", [tmp_path])
+
+ assert result is None
+ assert not (segment / "preview.png").exists()
+
+
+def test_duplicate_thumbnail_requests_share_one_generation_job(monkeypatch, tmp_path):
+ segment = _make_segment(tmp_path)
+ (segment / "qcamera.ts").write_bytes(b"video")
+ release = threading.Event()
+ started = threading.Event()
+ calls = []
+
+ def generate(preview_path):
+ calls.append(preview_path)
+ started.set()
+ release.wait(timeout=2)
+ preview_path.write_bytes(b"png")
+ return preview_path
+
+ monkeypatch.setattr(the_galaxy, "_generate_route_thumbnail", generate)
+ relative_path = f"{ROUTE_NAME}--0/preview.png"
+ with ThreadPoolExecutor(max_workers=2) as callers:
+ first = callers.submit(the_galaxy._get_or_create_route_thumbnail, relative_path, [tmp_path])
+ assert started.wait(timeout=1)
+ second = callers.submit(the_galaxy._get_or_create_route_thumbnail, relative_path, [tmp_path])
+ time.sleep(0.05)
+ release.set()
+ assert first.result(timeout=1) == segment / "preview.png"
+ assert second.result(timeout=1) == segment / "preview.png"
+
+ assert len(calls) == 1
+ assert the_galaxy._ROUTE_THUMBNAIL_EXECUTOR._max_workers == 2
+
+
+def test_timed_out_thumbnail_job_stays_deduplicated_until_completion(monkeypatch, tmp_path):
+ segment = _make_segment(tmp_path)
+ (segment / "qcamera.ts").write_bytes(b"video")
+ release = threading.Event()
+ started = threading.Event()
+ calls = []
+
+ def generate(preview_path):
+ calls.append(preview_path)
+ started.set()
+ release.wait(timeout=2)
+ preview_path.write_bytes(b"png")
+ return preview_path
+
+ monkeypatch.setattr(the_galaxy, "_generate_route_thumbnail", generate)
+ monkeypatch.setattr(the_galaxy, "ROUTE_THUMBNAIL_WAIT_SECONDS", 0.01)
+ relative_path = f"{ROUTE_NAME}--0/preview.png"
+ preview_key = str((tmp_path / f"{ROUTE_NAME}--0" / "preview.png").resolve())
+
+ assert the_galaxy._get_or_create_route_thumbnail(relative_path, [tmp_path]) is None
+ assert started.is_set()
+ assert preview_key in the_galaxy._ROUTE_THUMBNAIL_FUTURES
+
+ # A retry while the original job is still running must reuse that job.
+ assert the_galaxy._get_or_create_route_thumbnail(relative_path, [tmp_path]) is None
+ assert len(calls) == 1
+
+ release.set()
+ for _ in range(100):
+ if preview_key not in the_galaxy._ROUTE_THUMBNAIL_FUTURES:
+ break
+ time.sleep(0.01)
+
+ assert preview_key not in the_galaxy._ROUTE_THUMBNAIL_FUTURES
+ assert the_galaxy._get_or_create_route_thumbnail(relative_path, [tmp_path]) == segment / "preview.png"
+ assert len(calls) == 1
+
+
+def test_routes_endpoint_uses_sse_no_buffering_headers(monkeypatch, tmp_path):
+ segment = _make_segment(tmp_path)
+ (segment / "qlog.zst").write_bytes(b"log")
+ monkeypatch.setattr(utilities, "get_route_start_time", lambda path: datetime(2026, 8, 26, tzinfo=timezone.utc))
+ client = _make_client(monkeypatch, tmp_path)
+
+ response = client.get("/api/routes")
+
+ assert response.status_code == 200
+ assert response.mimetype == "text/event-stream"
+ assert response.headers["X-Accel-Buffering"] == "no"
+ assert "no-cache" in response.headers["Cache-Control"]
+ assert b'"progress": 1' in response.data
+ assert b'"startedAt": "2026-08-26T00:00:00Z"' in response.data
+
+
+def test_thumbnail_endpoint_sets_cache_headers(monkeypatch, tmp_path):
+ segment = _make_segment(tmp_path)
+ preview = segment / "preview.png"
+ preview.write_bytes(b"not-a-real-png-but-send-file-does-not-mind")
+ client = _make_client(monkeypatch, tmp_path)
+
+ with client.get(f"/thumbnails/{ROUTE_NAME}--0/preview.png") as response:
+ assert response.status_code == 200
+ assert response.mimetype == "image/png"
+ assert response.headers["Cache-Control"] == f"public, max-age={the_galaxy.ROUTE_THUMBNAIL_CACHE_SECONDS}"
+ assert response.data == preview.read_bytes()
+
+
+def test_rename_and_reset_keep_logs_and_use_both_reset_urls(monkeypatch, tmp_path):
+ segments = [_make_segment(tmp_path, segment_num=number) for number in (0, 3)]
+ for segment in segments:
+ (segment / "qlog.zst").write_bytes(b"log")
+ (segment / "Old_name").touch()
+ monkeypatch.setattr(utilities, "get_route_start_time", lambda path: datetime(2026, 8, 26, tzinfo=timezone.utc))
+ client = _make_client(monkeypatch, tmp_path)
+
+ renamed = client.post("/api/routes/rename", json={"old": ROUTE_NAME, "new": "New name"})
+ assert renamed.status_code == 200
+ assert renamed.get_json()["name"] == "New_name"
+ assert all((segment / "New_name").exists() for segment in segments)
+ assert all((segment / "qlog.zst").read_bytes() == b"log" for segment in segments)
+
+ reset = client.post("/api/routes/reset_name", json={"name": ROUTE_NAME})
+ assert reset.status_code == 200
+ assert reset.get_json()["timestamp"].startswith("2026-08-26")
+ assert all(not (segment / "New_name").exists() for segment in segments)
+ assert all((segment / "qlog.zst").exists() for segment in segments)
+
+ # The legacy URL remains available for older clients.
+ for segment in segments:
+ (segment / "Another_name").touch()
+ assert client.post("/api/routes/clear_name", json={"name": ROUTE_NAME}).status_code == 200
+
+
+def test_preserve_unpreserve_and_delete_route_endpoints(monkeypatch, tmp_path):
+ segment = _make_segment(tmp_path)
+ client = _make_client(monkeypatch, tmp_path)
+ attributes = set()
+ deleted = []
+ monkeypatch.setattr(the_galaxy, "PRESERVE_COUNT", 10)
+ monkeypatch.setattr(the_galaxy.os, "listxattr", lambda path: list(attributes), raising=False)
+ monkeypatch.setattr(the_galaxy.os, "getxattr", lambda path, name: the_galaxy.PRESERVE_ATTR_VALUE, raising=False)
+ monkeypatch.setattr(the_galaxy.os, "setxattr", lambda path, name, value: attributes.add(name), raising=False)
+ monkeypatch.setattr(the_galaxy.os, "removexattr", lambda path, name: attributes.discard(name), raising=False)
+ monkeypatch.setattr(the_galaxy, "delete_file", deleted.append)
+
+ assert client.post(f"/api/routes/{ROUTE_NAME}/preserve").status_code == 200
+ assert the_galaxy.PRESERVE_ATTR_NAME in attributes
+ assert client.delete(f"/api/routes/{ROUTE_NAME}/preserve").status_code == 200
+ assert the_galaxy.PRESERVE_ATTR_NAME not in attributes
+ assert client.delete(f"/api/routes/{ROUTE_NAME}").status_code == 200
+ assert deleted == [str(segment)]
+
+
+def test_preserve_follows_the_first_surviving_segment(monkeypatch, tmp_path):
+ segment = _make_segment(tmp_path, segment_num=3) # --0 and --1 already aged out
+ client = _make_client(monkeypatch, tmp_path)
+ attributes = {}
+ monkeypatch.setattr(the_galaxy, "PRESERVE_COUNT", 10)
+ monkeypatch.setattr(the_galaxy.os, "listxattr", lambda path: list(attributes.get(str(path), ())), raising=False)
+ monkeypatch.setattr(the_galaxy.os, "getxattr", lambda path, name: the_galaxy.PRESERVE_ATTR_VALUE, raising=False)
+ monkeypatch.setattr(the_galaxy.os, "setxattr", lambda path, name, value: attributes.setdefault(str(path), set()).add(name), raising=False)
+ monkeypatch.setattr(the_galaxy.os, "removexattr", lambda path, name: attributes[str(path)].discard(name), raising=False)
+
+ assert client.post(f"/api/routes/{ROUTE_NAME}/preserve").status_code == 200
+ assert attributes == {str(segment): {the_galaxy.PRESERVE_ATTR_NAME}}
+ assert utilities.process_route(str(tmp_path) + "/", ROUTE_NAME, 1, 3)["is_preserved"] is True
+
+ assert client.delete(f"/api/routes/{ROUTE_NAME}/preserve").status_code == 200
+ assert attributes[str(segment)] == set()
+
+
+def test_preserve_limit_counts_routes_not_segments(monkeypatch, tmp_path):
+ for segment_num in (5, 6, 7):
+ _make_segment(tmp_path, segment_num=segment_num)
+ client = _make_client(monkeypatch, tmp_path)
+ monkeypatch.setattr(the_galaxy, "PRESERVE_COUNT", 1)
+ monkeypatch.setattr(the_galaxy.os, "listxattr", lambda path: [the_galaxy.PRESERVE_ATTR_NAME], raising=False)
+ monkeypatch.setattr(the_galaxy.os, "getxattr", lambda path, name: the_galaxy.PRESERVE_ATTR_VALUE, raising=False)
+ monkeypatch.setattr(the_galaxy.os, "setxattr", lambda path, name, value: None, raising=False)
+
+ # Three preserved segments belong to one route, so the cap of 1 is not already spent on it.
+ assert client.post(f"/api/routes/{ROUTE_NAME}/preserve").status_code == 200
+ assert client.post("/api/routes/00000099--9f0a7bdf9c/preserve").status_code == 400
+
+
+def test_delete_all_non_preserved_keeps_entire_preserved_route_across_roots(monkeypatch, tmp_path):
+ standard = tmp_path / "standard"
+ high_resolution = tmp_path / "high_resolution"
+ preserved_route = ROUTE_NAME
+ ordinary_route = "0000006b--9f0a7bdf9d"
+ preserved_marker = _make_segment(standard, preserved_route, 3)
+ preserved_other_root = _make_segment(high_resolution, preserved_route, 4)
+ ordinary_standard = _make_segment(standard, ordinary_route, 0)
+ ordinary_other_root = _make_segment(high_resolution, ordinary_route, 1)
+ unrelated = high_resolution / "video_cache"
+ unrelated.mkdir()
+
+ client = _make_client(monkeypatch, standard)
+ monkeypatch.setattr(the_galaxy, "FOOTAGE_PATHS", [str(standard), str(high_resolution)])
+ monkeypatch.setattr(utilities, "has_preserve_attr", lambda path: path == str(preserved_marker))
+ monkeypatch.setattr(utilities, "stop_dashboard_background_analysis", lambda: None)
+ monkeypatch.setattr(the_galaxy, "delete_file", lambda path: Path(path).rmdir())
+ history_calls = []
+ monkeypatch.setattr(utilities, "clear_dashboard_route_history", lambda params, retained_route_names=None: history_calls.append(retained_route_names) or 1)
+ factory_delete_calls = []
+ monkeypatch.setattr(the_galaxy, "_run_factory_reset_delete", factory_delete_calls.append)
+
+ response = client.delete("/api/routes/delete_all?include_preserved=false")
+
+ assert response.status_code == 200
+ assert response.get_json()["deletedRoutes"] == 1
+ assert response.get_json()["preservedRoutes"] == 1
+ assert preserved_marker.is_dir()
+ assert preserved_other_root.is_dir()
+ assert not ordinary_standard.exists()
+ assert not ordinary_other_root.exists()
+ assert unrelated.is_dir()
+ assert history_calls == [{preserved_route}]
+ assert factory_delete_calls == []
+
+
+def test_delete_all_including_preserved_keeps_existing_full_wipe_behavior(monkeypatch, tmp_path):
+ first_root = tmp_path / "standard"
+ second_root = tmp_path / "high_resolution"
+ _make_segment(first_root)
+ _make_segment(second_root)
+ client = _make_client(monkeypatch, first_root)
+ monkeypatch.setattr(the_galaxy, "FOOTAGE_PATHS", [str(first_root) + "/", str(second_root), str(first_root)])
+ monkeypatch.setattr(utilities, "stop_dashboard_background_analysis", lambda: None)
+ history_calls = []
+ monkeypatch.setattr(utilities, "clear_dashboard_route_history", lambda params, retained_route_names=None: history_calls.append(retained_route_names) or 2)
+ factory_delete_calls = []
+ monkeypatch.setattr(the_galaxy, "_run_factory_reset_delete", factory_delete_calls.append)
+
+ response = client.delete("/api/routes/delete_all?include_preserved=true")
+
+ assert response.status_code == 200
+ assert factory_delete_calls == [str(first_root), str(second_root)]
+ assert history_calls == [None]
+ assert "including preserved routes" in response.get_json()["message"]
+
+
+def test_video_cache_evicts_oldest_instead_of_wiping_everything(monkeypatch, tmp_path):
+ """A tight disk used to delete every cached mp4, so playback re-muxed on every request."""
+ cache = tmp_path / "video_cache"
+ cache.mkdir()
+ monkeypatch.setattr(utilities, "VIDEO_CACHE_PATH", cache)
+ monkeypatch.setattr(utilities, "VIDEO_CACHE_MAX_BYTES", 300)
+
+ for index in range(4):
+ entry = cache / f"{index}.mp4"
+ entry.write_bytes(b"x" * 100)
+ os.utime(entry, (1000 + index, 1000 + index))
+
+ utilities._prune_video_cache()
+
+ survivors = sorted(path.name for path in cache.glob("*.mp4"))
+ # Budget is 300 bytes of 400 used, so only the oldest goes.
+ assert survivors == ["1.mp4", "2.mp4", "3.mp4"]
+
+
+def test_video_cache_never_evicts_the_entry_being_written(monkeypatch, tmp_path):
+ cache = tmp_path / "video_cache"
+ cache.mkdir()
+ monkeypatch.setattr(utilities, "VIDEO_CACHE_PATH", cache)
+ monkeypatch.setattr(utilities, "VIDEO_CACHE_MAX_BYTES", 50)
+
+ for index in range(3):
+ entry = cache / f"{index}.mp4"
+ entry.write_bytes(b"x" * 100)
+ os.utime(entry, (1000 + index, 1000 + index))
+
+ keep = cache / "0.mp4"
+ utilities._prune_video_cache(keep_path=keep)
+
+ assert keep.exists()
+
+
+def test_combined_video_streams_fragmented_mp4_without_a_full_cache_file(monkeypatch, tmp_path):
+ cache = tmp_path / "video_cache"
+ first = tmp_path / "first.hevc"
+ second = tmp_path / "second.hevc"
+ first.write_bytes(b"first")
+ second.write_bytes(b"second")
+ monkeypatch.setattr(utilities, "VIDEO_CACHE_PATH", cache)
+ captured = {}
+
+ class FakeProcess:
+ def __init__(self):
+ self.stdout = io.BytesIO(b"streamed-video")
+ self.returncode = None
+
+ def wait(self, timeout=None):
+ self.returncode = 0
+ return 0
+
+ def poll(self):
+ return self.returncode
+
+ def popen(command, **kwargs):
+ captured["command"] = command
+ list_path = Path(command[command.index("-i") + 1])
+ captured["list_path"] = list_path
+ captured["list_contents"] = list_path.read_text()
+ return FakeProcess()
+
+ monkeypatch.setattr(utilities.subprocess, "Popen", popen)
+
+ payload = b"".join(utilities.ffmpeg_stream_concatenated_mp4([first, second], chunk_size=4))
+
+ assert payload == b"streamed-video"
+ assert "frag_keyframe+empty_moov+default_base_moof" in captured["command"]
+ assert captured["command"][-1] == "pipe:1"
+ assert captured["list_contents"] == f"file '{first}'\nfile '{second}'\n"
+ assert not captured["list_path"].exists()
+ assert not list(cache.glob("*.mp4"))
+
+
+def test_combined_video_stream_has_a_hard_timeout(monkeypatch, tmp_path):
+ cache = tmp_path / "video_cache"
+ source = tmp_path / "first.hevc"
+ source.write_bytes(b"first")
+ monkeypatch.setattr(utilities, "VIDEO_CACHE_PATH", cache)
+ monkeypatch.setattr(utilities, "VIDEO_STREAM_TIMEOUT_SECONDS", 0.01)
+
+ class HangingStdout:
+ def read(self, _):
+ time.sleep(10)
+ return b""
+
+ def close(self):
+ pass
+
+ class HangingProcess:
+ def __init__(self):
+ self.stdout = HangingStdout()
+ self.returncode = None
+ self.terminated = False
+
+ def poll(self):
+ return self.returncode
+
+ def terminate(self):
+ self.terminated = True
+ self.returncode = -15
+
+ def wait(self, timeout=None):
+ del timeout
+ return self.returncode
+
+ process = HangingProcess()
+ monkeypatch.setattr(utilities.subprocess, "Popen", lambda *args, **kwargs: process)
+
+ with pytest.raises(TimeoutError, match="Timed out streaming"):
+ b"".join(utilities.ffmpeg_stream_concatenated_mp4([source], chunk_size=4))
+
+ assert process.terminated
+ assert not list(cache.glob("route-download-*.txt"))
+
+
+def test_route_endpoints_reject_invalid_names(monkeypatch, tmp_path):
+ client = _make_client(monkeypatch, tmp_path)
+
+ for method, path in (
+ (client.delete, "/api/routes/not-a-route"),
+ (client.post, "/api/routes/not-a-route/preserve"),
+ (client.delete, "/api/routes/not-a-route/preserve"),
+ (client.get, "/api/routes/not-a-route"),
+ (client.get, "/video/not-a-route/combined"),
+ ):
+ assert method(path).status_code == 400
+
+
+def _stub_remux(monkeypatch, tmp_path, payload=b"wrapped-video"):
+ """Stand in for the ffmpeg remux, returning a real file so send_file can stream it."""
+ wrapped = tmp_path / "wrapped.mp4"
+ wrapped.write_bytes(payload)
+ monkeypatch.setattr(utilities, "ffmpeg_mp4_wrap_to_path", lambda path: wrapped)
+ return wrapped
+
+
+def test_sparse_route_metadata_and_video_downloads(monkeypatch, tmp_path):
+ segments = [_make_segment(tmp_path, segment_num=number) for number in (0, 3, 11)]
+ for segment in segments:
+ (segment / "fcamera.hevc").write_bytes(b"hevc")
+ monkeypatch.setattr(utilities, "get_route_start_time", lambda path: datetime(2026, 8, 26, tzinfo=timezone.utc))
+ _stub_remux(monkeypatch, tmp_path)
+ monkeypatch.setattr(utilities, "ffmpeg_stream_concatenated_mp4", lambda paths: iter((b"combined-", b"video")))
+ client = _make_client(monkeypatch, tmp_path)
+
+ metadata = client.get(f"/api/routes/{ROUTE_NAME}")
+ assert metadata.status_code == 200
+ assert metadata.get_json()["segment_urls"] == [f"/video/{ROUTE_NAME}--{number}" for number in (0, 3, 11)]
+ # One minute per segment, without probing each one with ffprobe.
+ assert metadata.get_json()["total_duration"] == 180
+
+ with client.get(f"/video/{ROUTE_NAME}--3?camera=forward") as segment_video:
+ assert segment_video.status_code == 200
+ assert segment_video.mimetype == "video/mp4"
+ assert segment_video.data == b"wrapped-video"
+
+ with client.get(f"/video/{ROUTE_NAME}/combined?camera=forward") as combined_video:
+ assert combined_video.status_code == 200
+ assert combined_video.mimetype == "video/mp4"
+ assert combined_video.data == b"combined-video"
+ assert combined_video.headers["X-Accel-Buffering"] == "no"
+
+
+def test_route_metadata_never_probes_segments_with_ffprobe(monkeypatch, tmp_path):
+ """Probing each segment put one subprocess per segment in front of playback."""
+ for number in (0, 1, 2):
+ segment = _make_segment(tmp_path, segment_num=number)
+ (segment / "fcamera.hevc").write_bytes(b"hevc")
+
+ def explode(path):
+ raise AssertionError(f"ffprobe must stay off the route metadata path: {path}")
+
+ monkeypatch.setattr(utilities, "get_video_duration", explode)
+ monkeypatch.setattr(utilities, "get_route_start_time", lambda path: datetime(2026, 8, 26, tzinfo=timezone.utc))
+ client = _make_client(monkeypatch, tmp_path)
+
+ metadata = client.get(f"/api/routes/{ROUTE_NAME}")
+ assert metadata.status_code == 200
+ assert metadata.get_json()["total_duration"] == 180
+
+
+def test_low_quality_serves_the_wrapped_qcamera_preview(monkeypatch, tmp_path):
+ """qcamera.ts is tiny, but it still needs the mp4 wrap - MPEG-TS will not play in a