From 85cbbf3072d9eb3cbc216e00544d2d346c8351cb Mon Sep 17 00:00:00 2001 From: Michael Dickinson Date: Sun, 17 May 2026 13:05:56 -0700 Subject: [PATCH 1/7] disable saving captures to odlc --- projects/web-backend/src/vision/views.py | 73 +++++++++---------- .../src/hooks/useWebRTCConnection.ts | 1 - 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/projects/web-backend/src/vision/views.py b/projects/web-backend/src/vision/views.py index bbb7dee..f61559c 100644 --- a/projects/web-backend/src/vision/views.py +++ b/projects/web-backend/src/vision/views.py @@ -15,9 +15,6 @@ download_archive_object, list_archive_dates, list_archive_records, - upload_odlc_depth, - upload_odlc_image, - upload_odlc_metadata, ) ODLC_SESSIONS_DIR = Path(__file__).resolve().parent.parent.parent / "odlc_sessions" @@ -113,39 +110,39 @@ def post_odlc_record(request, session_id: str): if not isinstance(record, dict) or not record.get("id"): return JsonResponse({"error": "Invalid record"}, status=400) - image = record.get("image") or {} - received_at = record.get("receivedAt") - image_b64 = image.get("image_data") - depth_b64 = image.get("depth_data") - if image_b64 and not image.get("image_url"): - try: - record["image"]["image_url"] = upload_odlc_image( - record["id"], image_b64, received_at - ) - except Exception as e: - print(f"S3 image upload failed for record {record['id']}: {e}") - if depth_b64 and not image.get("depth_url"): - try: - record["image"]["depth_url"] = upload_odlc_depth( - record["id"], depth_b64, received_at - ) - except Exception as e: - print(f"S3 depth upload failed for record {record['id']}: {e}") - if image: - try: - upload_odlc_metadata( - record["id"], - received_at, - { - "boundingBox": image.get("bounding_box"), - "confidenceLevel": image.get("confidence_level"), - "yawDeg": image.get("yaw_deg"), - "colorDetection": image.get("color_detection"), - "heading": record.get("metadata"), - }, - ) - except Exception as e: - print(f"S3 metadata upload failed for record {record['id']}: {e}") + # image = record.get("image") or {} + # received_at = record.get("receivedAt") + # image_b64 = image.get("image_data") + # depth_b64 = image.get("depth_data") + # if image_b64 and not image.get("image_url"): + # try: + # record["image"]["image_url"] = upload_odlc_image( + # record["id"], image_b64, received_at + # ) + # except Exception as e: + # print(f"S3 image upload failed for record {record['id']}: {e}") + # if depth_b64 and not image.get("depth_url"): + # try: + # record["image"]["depth_url"] = upload_odlc_depth( + # record["id"], depth_b64, received_at + # ) + # except Exception as e: + # print(f"S3 depth upload failed for record {record['id']}: {e}") + # if image: + # try: + # upload_odlc_metadata( + # record["id"], + # received_at, + # { + # "boundingBox": image.get("bounding_box"), + # "confidenceLevel": image.get("confidence_level"), + # "yawDeg": image.get("yaw_deg"), + # "colorDetection": image.get("color_detection"), + # "heading": record.get("metadata"), + # }, + # ) + # except Exception as e: + # print(f"S3 metadata upload failed for record {record['id']}: {e}") with _SESSION_LOCK: data = _read_session(session_id) or _empty_session(session_id) @@ -221,9 +218,7 @@ def get_archive_object(request): """ key = (request.GET.get("key") or "").strip() if not key: - return JsonResponse( - {"error": "Missing 'key' query parameter"}, status=400 - ) + return JsonResponse({"error": "Missing 'key' query parameter"}, status=400) try: body, content_type = download_archive_object(key) except Exception as e: diff --git a/projects/web-frontend/src/hooks/useWebRTCConnection.ts b/projects/web-frontend/src/hooks/useWebRTCConnection.ts index a2fe05c..694e545 100644 --- a/projects/web-frontend/src/hooks/useWebRTCConnection.ts +++ b/projects/web-frontend/src/hooks/useWebRTCConnection.ts @@ -170,7 +170,6 @@ export function useWebRTCConnection(): UseWebRTCConnectionResult { return; } - console.log("📸 ODLC Image Message Parsed:", parsedMessage); const result = OdlcImageSchema.safeParse(parsedMessage); console.log("📸 ODLC Image Validation Result:", result); if (!result.success) { From dc4001103abd96b6a269469dff400fbb91341b01 Mon Sep 17 00:00:00 2001 From: Michael Dickinson Date: Sun, 17 May 2026 13:25:54 -0700 Subject: [PATCH 2/7] Polling endpoint for new images --- projects/web-backend/src/vision/views.py | 14 ++++++++ projects/web-frontend/src/api/endpoints.ts | 8 +++-- .../web-frontend/src/routes/OdlcImages.tsx | 34 ++++++++++++++++++- .../src/store/slices/dataSlice.ts | 9 +++++ .../src/store/thunks/odlcSessionThunks.ts | 21 +++++++++++- 5 files changed, 82 insertions(+), 4 deletions(-) diff --git a/projects/web-backend/src/vision/views.py b/projects/web-backend/src/vision/views.py index f61559c..38b0b5e 100644 --- a/projects/web-backend/src/vision/views.py +++ b/projects/web-backend/src/vision/views.py @@ -94,6 +94,20 @@ def get_odlc_session(request, session_id: str): data = _read_session(session_id) if data is None: return JsonResponse({"error": "Session not found"}, status=404) + since_raw = request.GET.get("since") + if since_raw is not None: + try: + since = int(since_raw) + data = { + **data, + "records": [ + r + for r in data.get("records", []) + if r.get("receivedAt", 0) > since + ], + } + except ValueError: + return JsonResponse({"error": "Invalid 'since' parameter"}, status=400) return JsonResponse(data) except Exception as e: print(f"Error reading ODLC session: {e}") diff --git a/projects/web-frontend/src/api/endpoints.ts b/projects/web-frontend/src/api/endpoints.ts index 1053856..d6a3977 100644 --- a/projects/web-frontend/src/api/endpoints.ts +++ b/projects/web-frontend/src/api/endpoints.ts @@ -105,9 +105,13 @@ export const getDroneParameter = async ( export type OdlcSessionPayload = z.infer; -export const getOdlcSession = async (sessionId: string): Promise => { +export const getOdlcSession = async ( + sessionId: string, + options?: { since?: number }, +): Promise => { try { - const response = await api.get(`/vision/odlc-session/${sessionId}/`); + const params = options?.since !== undefined ? { since: options.since } : undefined; + const response = await api.get(`/vision/odlc-session/${sessionId}/`, { params }); return OdlcSessionSchema.parse(response.data); } catch (err: unknown) { if (typeof err === "object" && err !== null && "response" in err) { diff --git a/projects/web-frontend/src/routes/OdlcImages.tsx b/projects/web-frontend/src/routes/OdlcImages.tsx index 243a968..2026cb1 100644 --- a/projects/web-frontend/src/routes/OdlcImages.tsx +++ b/projects/web-frontend/src/routes/OdlcImages.tsx @@ -37,7 +37,11 @@ import { } from "../store/slices/dataSlice"; import ImageAnnotationOverlay from "../components/ImageAnnotationOverlay"; import SessionIdDisplay from "../components/SessionIdDisplay"; -import { syncOdlcImageToBackend, appendOdlcImageFromAircraft } from "../store/thunks/odlcSessionThunks"; +import { + syncOdlcImageToBackend, + appendOdlcImageFromAircraft, + pollOdlcSession, +} from "../store/thunks/odlcSessionThunks"; import type { OdlcImageRecord } from "../store/slices/dataSlice"; import { classifyOdlcColor, ODLC_COLORS, ODLC_COLOR_LABELS } from "../utils/odlcColorGroup"; import type { OdlcColor, RGB } from "../utils/odlcColorGroup"; @@ -147,6 +151,7 @@ export default function OdlcImages() { const [flaggedOnly, setFlaggedOnly] = useState(false); const [sortBy, setSortBy] = useState("recency"); const [colorFilter, setColorFilter] = useState("all"); + const [autoRefresh, setAutoRefresh] = useState(false); const [highlightedAnnotationId, setHighlightedAnnotationId] = useState(null); const dispatch = useAppDispatch(); @@ -164,6 +169,27 @@ export default function OdlcImages() { // Callback ref: fires on attach/detach so we measure correctly even when // the list container isn't in the DOM on first render (e.g. empty state // then session restore populates records and mounts the main layout). + const pollIntervalRef = useRef | null>(null); + + useEffect(() => { + if (autoRefresh) { + pollIntervalRef.current = setInterval(() => { + dispatch(pollOdlcSession()); + }, 1000); + } else { + if (pollIntervalRef.current !== null) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + } + return () => { + if (pollIntervalRef.current !== null) { + clearInterval(pollIntervalRef.current); + pollIntervalRef.current = null; + } + }; + }, [autoRefresh, dispatch]); + const resizeObserverRef = useRef(null); const listContainerCallbackRef = useCallback((el: HTMLDivElement | null) => { resizeObserverRef.current?.disconnect(); @@ -341,6 +367,12 @@ export default function OdlcImages() { Filters + setAutoRefresh(v)} /> + } + label={Auto-refresh} + /> setFlaggedOnly(v)} /> diff --git a/projects/web-frontend/src/store/slices/dataSlice.ts b/projects/web-frontend/src/store/slices/dataSlice.ts index 2b49fb5..3b61d2f 100644 --- a/projects/web-frontend/src/store/slices/dataSlice.ts +++ b/projects/web-frontend/src/store/slices/dataSlice.ts @@ -162,6 +162,14 @@ const dataSlice = createSlice({ state.odlcImageRecords = []; state.selectedOdlcImageId = null; }, + mergeOdlcImageRecords: (state, action: PayloadAction) => { + const existingIds = new Set(state.odlcImageRecords.map((r) => r.id)); + for (const record of action.payload) { + if (!existingIds.has(record.id)) { + state.odlcImageRecords.push(record); + } + } + }, setSelectedOdlcImage: (state, action: PayloadAction) => { state.selectedOdlcImageId = action.payload; }, @@ -247,6 +255,7 @@ export const { appendOdlcImage, loadOdlcImageRecords, clearOdlcImages, + mergeOdlcImageRecords, setSelectedOdlcImage, updateOdlcImageFlag, updateOdlcImageTextInput, diff --git a/projects/web-frontend/src/store/thunks/odlcSessionThunks.ts b/projects/web-frontend/src/store/thunks/odlcSessionThunks.ts index 72cf61e..0b559e8 100644 --- a/projects/web-frontend/src/store/thunks/odlcSessionThunks.ts +++ b/projects/web-frontend/src/store/thunks/odlcSessionThunks.ts @@ -1,6 +1,6 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import { getOdlcSession, patchOdlcRecord } from "../../api/endpoints"; -import { appendOdlcImage, clearOdlcImages, loadOdlcImageRecords } from "../slices/dataSlice"; +import { appendOdlcImage, clearOdlcImages, loadOdlcImageRecords, mergeOdlcImageRecords } from "../slices/dataSlice"; import { setOdlcSessionId } from "../slices/appSlice"; import { AppDispatch, RootState } from "../store"; import type { OdlcImage } from "../../schemas/odlc"; @@ -40,6 +40,25 @@ export const newOdlcSession = createAsyncThunk }, ); +export const pollOdlcSession = createAsyncThunk( + "odlcSession/poll", + async (_, { dispatch, getState }) => { + const state = getState(); + const sessionId = state.app.odlcSessionId; + if (!sessionId) return; + const records = state.data.odlcImageRecords; + const since = records.length > 0 ? Math.max(...records.map((r) => r.receivedAt)) : 0; + try { + const session = await getOdlcSession(sessionId, { since }); + if (session && session.records.length > 0) { + dispatch(mergeOdlcImageRecords(session.records)); + } + } catch (err) { + console.error("Failed to poll ODLC session:", err); + } + }, +); + export const restoreOdlcSession = createAsyncThunk( "odlcSession/restore", async (_, { dispatch, getState }) => { From 4da6fb94f4c5e973abe8b7ba0c9027b67fce8199 Mon Sep 17 00:00:00 2001 From: Michael Dickinson Date: Sun, 17 May 2026 13:40:00 -0700 Subject: [PATCH 3/7] add sidecar to odlc_sessions json to make queries faster --- projects/web-backend/src/vision/urls.py | 12 ++- projects/web-backend/src/vision/views.py | 75 ++++++++++++++++++- projects/web-frontend/src/api/endpoints.ts | 21 +++++- .../src/store/thunks/odlcSessionThunks.ts | 11 ++- 4 files changed, 109 insertions(+), 10 deletions(-) diff --git a/projects/web-backend/src/vision/urls.py b/projects/web-backend/src/vision/urls.py index fff3add..ae0c95c 100644 --- a/projects/web-backend/src/vision/urls.py +++ b/projects/web-backend/src/vision/urls.py @@ -9,7 +9,8 @@ get_archive_object, get_archive_records_for_date, get_odlc_session, - patch_odlc_record, + get_odlc_session_index, + odlc_record, post_odlc_record, ) @@ -20,6 +21,11 @@ urlpatterns = [ path("", include(router.urls)), path("odlc-session//", get_odlc_session, name="get_odlc_session"), + path( + "odlc-session//index/", + get_odlc_session_index, + name="get_odlc_session_index", + ), path( "odlc-session//records/", post_odlc_record, @@ -27,8 +33,8 @@ ), path( "odlc-session//records//", - patch_odlc_record, - name="patch_odlc_record", + odlc_record, + name="odlc_record", ), path("deproject-pixel/", deproject_pixel, name="deproject_pixel"), path("archive/dates/", get_archive_dates, name="get_archive_dates"), diff --git a/projects/web-backend/src/vision/views.py b/projects/web-backend/src/vision/views.py index 38b0b5e..863ee28 100644 --- a/projects/web-backend/src/vision/views.py +++ b/projects/web-backend/src/vision/views.py @@ -26,6 +26,26 @@ def _session_path(session_id: str) -> Path: return ODLC_SESSIONS_DIR / f"{session_id}.json" +def _index_path(session_id: str) -> Path: + return ODLC_SESSIONS_DIR / f"{session_id}.index.json" + + +def _read_index(session_id: str) -> list[dict] | None: + path = _index_path(session_id) + if not path.exists(): + return None + return json.loads(path.read_text()) + + +def _write_index(session_id: str, records: list[dict]) -> None: + entries = [ + {"id": r["id"], "receivedAt": r.get("receivedAt", 0)} + for r in records + if r.get("id") and r.get("receivedAt") + ] + _index_path(session_id).write_text(json.dumps(entries)) + + def _read_session(session_id: str) -> dict | None: path = _session_path(session_id) if not path.exists(): @@ -36,6 +56,7 @@ def _read_session(session_id: str) -> dict | None: def _write_session(session_id: str, data: dict) -> None: ODLC_SESSIONS_DIR.mkdir(exist_ok=True) _session_path(session_id).write_text(json.dumps(data)) + _write_index(session_id, data.get("records", [])) def _empty_session(session_id: str) -> dict: @@ -116,6 +137,38 @@ def get_odlc_session(request, session_id: str): ) +@csrf_exempt +@require_http_methods(["GET"]) +def get_odlc_session_index(request, session_id: str): + try: + with _SESSION_LOCK: + index = _read_index(session_id) + if index is None: + data = _read_session(session_id) + if data is None: + return JsonResponse({"error": "Session not found"}, status=404) + records = data.get("records", []) + _write_index(session_id, records) + index = [ + {"id": r["id"], "receivedAt": r.get("receivedAt", 0)} + for r in records + if r.get("id") and r.get("receivedAt") + ] + since_raw = request.GET.get("since") + if since_raw is not None: + try: + since = int(since_raw) + index = [e for e in index if e.get("receivedAt", 0) > since] + except ValueError: + return JsonResponse({"error": "Invalid 'since' parameter"}, status=400) + return JsonResponse({"sessionId": session_id, "records": index}) + except Exception as e: + print(f"Error reading ODLC session index: {e}") + return JsonResponse( + {"error": "Internal server error", "details": str(e)}, status=500 + ) + + @csrf_exempt @require_http_methods(["POST"]) def post_odlc_record(request, session_id: str): @@ -247,8 +300,26 @@ def get_archive_object(request): @csrf_exempt -@require_http_methods(["PATCH"]) -def patch_odlc_record(request, session_id: str, record_id: str): +@require_http_methods(["GET", "PATCH"]) +def odlc_record(request, session_id: str, record_id: str): + if request.method == "GET": + try: + with _SESSION_LOCK: + data = _read_session(session_id) + if data is None: + return JsonResponse({"error": "Session not found"}, status=404) + record = next( + (r for r in data.get("records", []) if r.get("id") == record_id), None + ) + if record is None: + return JsonResponse({"error": "Record not found"}, status=404) + return JsonResponse(record) + except Exception as e: + print(f"Error reading ODLC record: {e}") + return JsonResponse( + {"error": "Internal server error", "details": str(e)}, status=500 + ) + try: updates = json.loads(request.body) if not isinstance(updates, dict): diff --git a/projects/web-frontend/src/api/endpoints.ts b/projects/web-frontend/src/api/endpoints.ts index d6a3977..04db08a 100644 --- a/projects/web-frontend/src/api/endpoints.ts +++ b/projects/web-frontend/src/api/endpoints.ts @@ -2,7 +2,7 @@ import { Waypoint } from "../types/Waypoint"; import { Route } from "../types/Route"; import api from "./api"; import { WaypointSchema, RouteSchema, PartialWaypointSchema } from "../schemas/waypoint"; -import { OdlcSessionSchema } from "../schemas/odlc"; +import { OdlcSessionSchema, OdlcImageRecordSchema } from "../schemas/odlc"; import type { OdlcImageRecord } from "../store/slices/dataSlice"; import type { z } from "zod"; @@ -134,6 +134,25 @@ export const patchOdlcRecord = async ( await api.patch(`/vision/odlc-session/${sessionId}/records/${recordId}/`, updates); }; +export const getNewOdlcRecordIds = async (sessionId: string, since: number): Promise => { + const response = await api.get(`/vision/odlc-session/${sessionId}/index/`, { params: { since } }); + const data = response.data as { records: { id: string }[] }; + return data.records.map((r) => r.id); +}; + +export const getOdlcRecord = async (sessionId: string, recordId: string): Promise => { + try { + const response = await api.get(`/vision/odlc-session/${sessionId}/records/${recordId}/`); + return OdlcImageRecordSchema.parse(response.data); + } catch (err: unknown) { + if (typeof err === "object" && err !== null && "response" in err) { + const status = (err as { response?: { status?: number } }).response?.status; + if (status === 404) return null; + } + throw err; + } +}; + /** * Deprojects a 2D pixel coordinate and depth value into a 3D point in camera space. * Calls the backend which implements the RealSense deprojection formula with diff --git a/projects/web-frontend/src/store/thunks/odlcSessionThunks.ts b/projects/web-frontend/src/store/thunks/odlcSessionThunks.ts index 0b559e8..eee9b4c 100644 --- a/projects/web-frontend/src/store/thunks/odlcSessionThunks.ts +++ b/projects/web-frontend/src/store/thunks/odlcSessionThunks.ts @@ -1,5 +1,5 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; -import { getOdlcSession, patchOdlcRecord } from "../../api/endpoints"; +import { getOdlcSession, getNewOdlcRecordIds, getOdlcRecord, patchOdlcRecord } from "../../api/endpoints"; import { appendOdlcImage, clearOdlcImages, loadOdlcImageRecords, mergeOdlcImageRecords } from "../slices/dataSlice"; import { setOdlcSessionId } from "../slices/appSlice"; import { AppDispatch, RootState } from "../store"; @@ -49,9 +49,12 @@ export const pollOdlcSession = createAsyncThunk 0 ? Math.max(...records.map((r) => r.receivedAt)) : 0; try { - const session = await getOdlcSession(sessionId, { since }); - if (session && session.records.length > 0) { - dispatch(mergeOdlcImageRecords(session.records)); + const newIds = await getNewOdlcRecordIds(sessionId, since); + if (newIds.length === 0) return; + const fetched = await Promise.all(newIds.map((id) => getOdlcRecord(sessionId, id))); + const valid = fetched.filter((r): r is NonNullable => r !== null); + if (valid.length > 0) { + dispatch(mergeOdlcImageRecords(valid)); } } catch (err) { console.error("Failed to poll ODLC session:", err); From 9384fe0afc22faf39e90e47a5692fa9b6b8e1738 Mon Sep 17 00:00:00 2001 From: vayer2005 Date: Sun, 17 May 2026 17:14:04 -0700 Subject: [PATCH 4/7] avoid refresh to page 1 in odlc --- projects/web-frontend/src/routes/OdlcImages.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/projects/web-frontend/src/routes/OdlcImages.tsx b/projects/web-frontend/src/routes/OdlcImages.tsx index 2026cb1..3730c56 100644 --- a/projects/web-frontend/src/routes/OdlcImages.tsx +++ b/projects/web-frontend/src/routes/OdlcImages.tsx @@ -218,10 +218,12 @@ export default function OdlcImages() { [filteredSorted, clampedPage, pageSize], ); - // Reset to first page when filters or pageSize change + // Reset to first page when filters change. Do not reset when pageSize changes: + // pageSize is driven by ResizeObserver and can flicker across row-count thresholds + // (scrollbar, flex layout), which was sending users back to page 1 while paging. useEffect(() => { setPage(0); - }, [flaggedOnly, sortBy, colorFilter, pageSize]); + }, [flaggedOnly, sortBy, colorFilter]); /** Flagged records in current filter/sort order (export includes only these). */ const flaggedForExport = useMemo(() => filteredSorted.filter((r) => r.flagged), [filteredSorted]); From 33be1914732f0a108855119f83978a11bccd111e Mon Sep 17 00:00:00 2001 From: Michael Dickinson Date: Sun, 17 May 2026 17:40:22 -0700 Subject: [PATCH 5/7] query for most recent session-id when restoring odlc sessions --- projects/web-backend/src/vision/urls.py | 4 +++ projects/web-backend/src/vision/views.py | 21 ++++++++++++ projects/web-frontend/src/api/api.ts | 2 +- projects/web-frontend/src/api/endpoints.ts | 13 +++++++ .../web-frontend/src/routes/OdlcImages.tsx | 34 ++++++------------- .../src/store/thunks/odlcSessionThunks.ts | 21 ++++++++---- 6 files changed, 64 insertions(+), 31 deletions(-) diff --git a/projects/web-backend/src/vision/urls.py b/projects/web-backend/src/vision/urls.py index ae0c95c..7e91718 100644 --- a/projects/web-backend/src/vision/urls.py +++ b/projects/web-backend/src/vision/urls.py @@ -8,6 +8,7 @@ get_archive_dates, get_archive_object, get_archive_records_for_date, + get_latest_odlc_session, get_odlc_session, get_odlc_session_index, odlc_record, @@ -20,6 +21,9 @@ urlpatterns = [ path("", include(router.urls)), + path( + "odlc-session/latest/", get_latest_odlc_session, name="get_latest_odlc_session" + ), path("odlc-session//", get_odlc_session, name="get_odlc_session"), path( "odlc-session//index/", diff --git a/projects/web-backend/src/vision/views.py b/projects/web-backend/src/vision/views.py index 863ee28..072baa1 100644 --- a/projects/web-backend/src/vision/views.py +++ b/projects/web-backend/src/vision/views.py @@ -107,6 +107,27 @@ def deproject_pixel(request): ) +@csrf_exempt +@require_http_methods(["GET"]) +def get_latest_odlc_session(request): + try: + session_files = [ + p + for p in ODLC_SESSIONS_DIR.glob("*.json") + if not p.name.endswith(".index.json") + ] + if not session_files: + return JsonResponse({"error": "No sessions found"}, status=404) + latest = max(session_files, key=lambda p: p.stat().st_mtime) + session_id = latest.stem + return JsonResponse({"sessionId": session_id}) + except Exception as e: + print(f"Error finding latest ODLC session: {e}") + return JsonResponse( + {"error": "Internal server error", "details": str(e)}, status=500 + ) + + @csrf_exempt @require_http_methods(["GET"]) def get_odlc_session(request, session_id: str): diff --git a/projects/web-frontend/src/api/api.ts b/projects/web-frontend/src/api/api.ts index ba0a454..5419cc9 100644 --- a/projects/web-frontend/src/api/api.ts +++ b/projects/web-frontend/src/api/api.ts @@ -1,7 +1,7 @@ import axios from "axios"; export const api = axios.create({ - baseURL: "http://localhost:8000/api/", + baseURL: "https://vittals-macbook-air.tail8164f3.ts.net/api", headers: { "Content-Type": "application/json", }, diff --git a/projects/web-frontend/src/api/endpoints.ts b/projects/web-frontend/src/api/endpoints.ts index 04db08a..16e91c3 100644 --- a/projects/web-frontend/src/api/endpoints.ts +++ b/projects/web-frontend/src/api/endpoints.ts @@ -105,6 +105,19 @@ export const getDroneParameter = async ( export type OdlcSessionPayload = z.infer; +export const getLatestOdlcSessionId = async (): Promise => { + try { + const response = await api.get("/vision/odlc-session/latest/"); + return (response.data as { sessionId: string }).sessionId; + } catch (err: unknown) { + if (typeof err === "object" && err !== null && "response" in err) { + const status = (err as { response?: { status?: number } }).response?.status; + if (status === 404) return null; + } + throw err; + } +}; + export const getOdlcSession = async ( sessionId: string, options?: { since?: number }, diff --git a/projects/web-frontend/src/routes/OdlcImages.tsx b/projects/web-frontend/src/routes/OdlcImages.tsx index 2026cb1..3f89cdc 100644 --- a/projects/web-frontend/src/routes/OdlcImages.tsx +++ b/projects/web-frontend/src/routes/OdlcImages.tsx @@ -319,28 +319,6 @@ export default function OdlcImages() { }); }, [selectedRecord]); - if (records.length === 0) { - return ( - - - - ODLC Images - - - - - No ODLC images captured yet. - - Connect to a WebRTC stream to receive images. - - - - - ); - } - return ( @@ -525,7 +503,17 @@ export default function OdlcImages() { position: "relative", }} > - {selectedRecord ? ( + {records.length === 0 ? ( + + No ODLC images captured yet. + + Connect to a WebRTC stream to receive images. + + + + ) : selectedRecord ? ( <> ( "odlcSession/restore", - async (_, { dispatch, getState }) => { - const sessionId = getState().app.odlcSessionId; - if (!sessionId) { - dispatch(newOdlcSession()); - return; - } + async (_, { dispatch }) => { try { + const sessionId = await getLatestOdlcSessionId(); + if (sessionId === null) { + dispatch(newOdlcSession()); + return; + } const session = await getOdlcSession(sessionId); if (session === null) { dispatch(newOdlcSession()); return; } + dispatch(setOdlcSessionId(sessionId)); dispatch(loadOdlcImageRecords(session.records)); } catch (err) { console.error("Failed to restore ODLC session:", err); From cf2885a67c00dd90d1f190ef3b5fbab99fc9a68e Mon Sep 17 00:00:00 2001 From: Michael Dickinson Date: Sun, 17 May 2026 17:46:14 -0700 Subject: [PATCH 6/7] revert setting api url to hardcode url --- projects/web-frontend/src/api/api.ts | 2 +- projects/web-frontend/src/routes/DepthArchive.tsx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/projects/web-frontend/src/api/api.ts b/projects/web-frontend/src/api/api.ts index 5419cc9..ba0a454 100644 --- a/projects/web-frontend/src/api/api.ts +++ b/projects/web-frontend/src/api/api.ts @@ -1,7 +1,7 @@ import axios from "axios"; export const api = axios.create({ - baseURL: "https://vittals-macbook-air.tail8164f3.ts.net/api", + baseURL: "http://localhost:8000/api/", headers: { "Content-Type": "application/json", }, diff --git a/projects/web-frontend/src/routes/DepthArchive.tsx b/projects/web-frontend/src/routes/DepthArchive.tsx index 38af9d9..2138bed 100644 --- a/projects/web-frontend/src/routes/DepthArchive.tsx +++ b/projects/web-frontend/src/routes/DepthArchive.tsx @@ -131,7 +131,6 @@ function createArchiveImage(entry: ArchiveRecord, colorBase64: string, depthBase }; } - function getArchiveItemLabel(record: OdlcImageRecord): string { const entryId = record.id.split(":").slice(1).join(":") || record.id; return `${new Date(record.receivedAt).toLocaleTimeString()} · ${entryId}`; @@ -783,7 +782,9 @@ export default function DepthArchive() { )} {selectedRecord.metadata.trim() || - `${selectedRecord.image.yaw_deg!.toFixed(1)}° ${yawToCompass(selectedRecord.image.yaw_deg!)}`} + `${selectedRecord.image.yaw_deg!.toFixed(1)}° ${yawToCompass( + selectedRecord.image.yaw_deg!, + )}`} )} From 48e3878de9d38426cf41d9c711d72d20d2be42fe Mon Sep 17 00:00:00 2001 From: Michael Dickinson Date: Sun, 17 May 2026 17:58:01 -0700 Subject: [PATCH 7/7] use backend_url env var --- projects/web-frontend/src/api/api.ts | 5 ++++- projects/web-frontend/src/api/socket.ts | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/projects/web-frontend/src/api/api.ts b/projects/web-frontend/src/api/api.ts index ba0a454..df240dd 100644 --- a/projects/web-frontend/src/api/api.ts +++ b/projects/web-frontend/src/api/api.ts @@ -1,10 +1,13 @@ import axios from "axios"; +export const backend_url = import.meta.env.VITE_BACKEND_URL || "http://localhost:8000/"; export const api = axios.create({ - baseURL: "http://localhost:8000/api/", + baseURL: backend_url + "api/", headers: { "Content-Type": "application/json", }, }); +console.log("Using backend url: ", backend_url); + export default api; diff --git a/projects/web-frontend/src/api/socket.ts b/projects/web-frontend/src/api/socket.ts index feb15ea..42d1218 100644 --- a/projects/web-frontend/src/api/socket.ts +++ b/projects/web-frontend/src/api/socket.ts @@ -1,6 +1,7 @@ import { io } from "socket.io-client"; +import { backend_url } from "./api"; -export const socket = io("http://localhost:8000", { +export const socket = io(backend_url, { autoConnect: true, });