From e342c2d9240650297648f327634ee2d2784489ff Mon Sep 17 00:00:00 2001 From: John Zhou Date: Tue, 2 Jun 2026 17:10:26 -0700 Subject: [PATCH 01/21] recording tweaks --- backend_py/src/routes/recordings.py | 92 ++++---- .../services/recordings/recordings_service.py | 32 ++- frontend/package-lock.json | 81 ++++++- frontend/package.json | 1 + .../components/recording-modals.tsx | 89 +++++--- .../recordings/components/recording-table.tsx | 13 +- .../components/dwe/recordings/recordings.tsx | 213 ++++++++++++++---- .../dwe/recordings/store/recording-store.tsx | 142 +++++++++--- frontend/src/components/ui/progress.tsx | 26 +++ 9 files changed, 531 insertions(+), 158 deletions(-) create mode 100644 frontend/src/components/ui/progress.tsx diff --git a/backend_py/src/routes/recordings.py b/backend_py/src/routes/recordings.py index d38ef411..db818ab8 100644 --- a/backend_py/src/routes/recordings.py +++ b/backend_py/src/routes/recordings.py @@ -6,6 +6,7 @@ and downloading all recordings as ZIP """ +import asyncio import os import shutil import time @@ -27,30 +28,36 @@ class DiskStatsResponse(BaseModel): used: int free: int - -# dict of timp file paths -download_tokens: dict[str, dict] = {} - +active_zip_jobs: dict[str, dict] = {} # Helpers def remove_file(path: str) -> None: if os.path.exists(path): os.remove(path) - -def clean_orphaned_tokens() -> None: - current_time = time.time() - expired_tokens = [] - - for token, data in download_tokens.items(): - if current_time - data["created_at"] > 30: # GET req not seen for 30 secs - expired_tokens.append(token) - - for token in expired_tokens: - data = download_tokens.pop(token) - # in case local zip file wasn't deleted by background task - remove_file(data["path"]) - +# for refreshes during downloads +async def auto_cleanup_job(job_id: str) -> None: + await asyncio.sleep(600) # Wait 10 minutes + if job_id in active_zip_jobs: + data = active_zip_jobs.pop(job_id) + if data.get("path"): + remove_file(data["path"]) + +def background_zip_worker(job_id: str, filenames: list[str], service: RecordingsService): + try: + zip_path = service.zip_recordings(filenames, active_jobs=active_zip_jobs, job_id=job_id) + + if zip_path == "CANCELLED": + active_zip_jobs.pop(job_id, None) + return + + if job_id in active_zip_jobs: + active_zip_jobs[job_id]["status"] = "ready" + active_zip_jobs[job_id]["path"] = zip_path + + except Exception as e: + if job_id in active_zip_jobs: + active_zip_jobs[job_id]["status"] = "error" @recordings_router.get("", summary="Get all recordings") def get_recordings(request: Request) -> list[RecordingInfo]: @@ -70,25 +77,32 @@ def get_disk_usage(request: Request) -> DiskStatsResponse: return DiskStatsResponse(total=total, used=used, free=free) -@recordings_router.post("/zip/prepare", summary="Zip files and generate token") -def prepare_zip_download( +@recordings_router.post("/zip/prepare", summary="Start background zip job") +def start_zip_job( request: Request, - filenames: list[str] = Body(...), # noqa: B008 + background_tasks: BackgroundTasks, + filenames: list[str] = Body(...), # noqa: B008 ) -> dict: - clean_orphaned_tokens() - - recordings_service: RecordingsService = request.app.state.recordings_service - - zip_file_path = recordings_service.zip_recordings(filenames) - - if not zip_file_path or not os.path.exists(zip_file_path): - raise HTTPException(status_code=404, detail="No recordings to zip") - - token = uuid.uuid4().hex - download_tokens[token] = {"path": zip_file_path, "created_at": time.time()} - - return {"token": token} - + job_id = uuid.uuid4().hex + active_zip_jobs[job_id] = {"status": "zipping", "cancel": False, "path": None, "created_at": time.time(), "progress": 0} + recordings_service = request.app.state.recordings_service + background_tasks.add_task(background_zip_worker, job_id, filenames, recordings_service) + background_tasks.add_task(auto_cleanup_job, job_id) + + return {"job_id": job_id} + +@recordings_router.get("/zip/status/{job_id}", summary="Check zip job status") +def check_zip_status(job_id: str) -> dict: + if job_id not in active_zip_jobs: + raise HTTPException(status_code=404, detail="Job not found") + job = active_zip_jobs[job_id] + return {"status": job["status"], "progress": job.get("progress", 0)} + +@recordings_router.post("/zip/cancel/{job_id}", summary="Cancel a zip job") +def cancel_zip_job(job_id: str) -> dict: + if job_id in active_zip_jobs: + active_zip_jobs[job_id]["cancel"] = True + return {"message": "Cancellation requested"} @recordings_router.get("/zip/download", summary="Download ZIP using token") def download_zip( @@ -96,13 +110,13 @@ def download_zip( background_tasks: BackgroundTasks, filename: str = "selected_recordings.zip", ) -> FileResponse: - if token not in download_tokens: + if token not in active_zip_jobs: raise HTTPException(status_code=404, detail="Invalid or expired download token") - token_data = download_tokens.pop(token) - zip_file_path = token_data["path"] + job_data = active_zip_jobs.pop(token) + zip_file_path = job_data.get("path") - if not os.path.exists(zip_file_path): + if not zip_file_path or not os.path.exists(zip_file_path): raise HTTPException(status_code=404, detail="Zip file not found") background_tasks.add_task(remove_file, zip_file_path) diff --git a/backend_py/src/services/recordings/recordings_service.py b/backend_py/src/services/recordings/recordings_service.py index dd88388a..e92abe55 100644 --- a/backend_py/src/services/recordings/recordings_service.py +++ b/backend_py/src/services/recordings/recordings_service.py @@ -162,24 +162,36 @@ def rename_recording( return self.recordings return None - def zip_recordings(self, filenames: list[str] | None = None) -> str | None: + def zip_recordings(self, filenames: list[str] | None = None, active_jobs: dict | None = None, job_id: str | None = None) -> str | None: self.get_recordings() if not self.recordings: return None + targets = [] + for recording in self.recordings: + full_name = f"{recording.name}.{recording.format}" + if filenames is not None and recording.name not in filenames and full_name not in filenames: + continue + targets.append((recording, full_name)) + + total_files = len(targets) + if total_files == 0: + return None + unique_id = uuid.uuid4().hex zip_filename = os.path.join(self.recordings_path, f"temp_{unique_id}.zip") with zipfile.ZipFile(zip_filename, "w") as zipf: - for recording in self.recordings: - full_name = f"{recording.name}.{recording.format}" - - if ( - filenames is not None - and recording.name not in filenames - and full_name not in filenames - ): - continue + for i, (recording, full_name) in enumerate(targets): + if active_jobs is not None and job_id is not None: + if active_jobs.get(job_id, {}).get("cancel", False): + zipf.close() + if os.path.exists(zip_filename): + os.remove(zip_filename) + return "CANCELLED" + + progress = int((i / total_files) * 100) + active_jobs[job_id]["progress"] = progress zipf.write(recording.path, arcname=full_name) return zip_filename diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a7f34aa9..a50fce57 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.3.6", "@radix-ui/react-scroll-area": "^1.2.6", "@radix-ui/react-select": "^2.1.6", @@ -127,6 +128,7 @@ "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -2075,6 +2077,68 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-radio-group": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.6.tgz", @@ -3462,6 +3526,7 @@ "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3477,6 +3542,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -3488,6 +3554,7 @@ "integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3534,6 +3601,7 @@ "integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.31.0", "@typescript-eslint/types": "8.31.0", @@ -3801,7 +3869,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/acorn": { "version": "8.15.0", @@ -3809,6 +3878,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4043,6 +4113,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", @@ -4536,6 +4607,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5121,6 +5193,7 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -6555,6 +6628,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -6740,6 +6814,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -6752,6 +6827,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -7399,6 +7475,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -7550,6 +7627,7 @@ "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7830,6 +7908,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/frontend/package.json b/frontend/package.json index 8371c99c..a8283a39 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.3.6", "@radix-ui/react-scroll-area": "^1.2.6", "@radix-ui/react-select": "^2.1.6", diff --git a/frontend/src/components/dwe/recordings/components/recording-modals.tsx b/frontend/src/components/dwe/recordings/components/recording-modals.tsx index cc11120c..6e2c5242 100644 --- a/frontend/src/components/dwe/recordings/components/recording-modals.tsx +++ b/frontend/src/components/dwe/recordings/components/recording-modals.tsx @@ -2,16 +2,6 @@ import { recordingsActions, recordingsState, } from "@/components/dwe/recordings/store/recording-store"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -146,44 +136,87 @@ export const RecordingModals = ({ baseUrl }: { baseUrl: string }) => { {/* Delete Dialog */} - 0} onOpenChange={(open) => !open && recordingsActions.closeDelete()} > - - + +
-
- Delete recording? - +
+ Delete recording? + This action cannot be undone. - +
- - - + + + + + + + {/* Cancel All Downloads Dialog */} + + !open && recordingsActions.closeCancelAllModal() + } + > + + +
+
+ +
+
+ Cancel all downloads? + + This will immediately stop all active zipping tasks. + Incomplete files will be deleted. + +
+
+
+ + + + +
+
); }; diff --git a/frontend/src/components/dwe/recordings/components/recording-table.tsx b/frontend/src/components/dwe/recordings/components/recording-table.tsx index 4ee50219..928679ab 100644 --- a/frontend/src/components/dwe/recordings/components/recording-table.tsx +++ b/frontend/src/components/dwe/recordings/components/recording-table.tsx @@ -227,12 +227,12 @@ export const RecordingTable = ({ e.stopPropagation()} > onSort("name")} > Name   @@ -261,6 +261,7 @@ export const RecordingTable = ({ Size   {sortColumn === "size" && (sortDirection === "asc" ? "▲" : "▼")} + @@ -324,18 +325,18 @@ export const RecordingTable = ({ /> - + {formatDate(recording.created)} - + {recording.duration} - + {formatFileSize( recording.size ? parseFloat(recording.size) : 0, )} - + - - + + + +
+ + {snap.selectedNames.length} selected + + +
+ ) : ( + + {snap.zipJobs.length} Download{snap.zipJobs.length > 1 ? "s" : ""} + + + +
+
+
+ {snap.zipJobs.map((job) => ( +
+
+
+ + Zipping{" "} + {job.totalFiles} items... + + +
+ +
+
+ ))} +
+
+
+ + )} ); }; diff --git a/frontend/src/components/dwe/recordings/store/recording-store.tsx b/frontend/src/components/dwe/recordings/store/recording-store.tsx index d91b5dac..2aa6205d 100644 --- a/frontend/src/components/dwe/recordings/store/recording-store.tsx +++ b/frontend/src/components/dwe/recordings/store/recording-store.tsx @@ -12,12 +12,21 @@ interface DiskStats { used: number; free: number; } + +interface ZipJob { + id: string; + progress: number; + status: "zipping" | "ready" | "error"; + totalFiles: number; +} interface RecordingsState { recordings: RecordingInfo[]; diskStats: DiskStats | null; selectedNames: string[]; loading: boolean; - zipDownloading: boolean; + zipJobs: ZipJob[]; + isZipDrawerMinimized: boolean; + isCancelAllZipModalOpen: boolean; // Modal Targets playTarget: RecordingInfo | null; @@ -44,7 +53,9 @@ export const recordingsState = proxy({ diskStats: null, selectedNames: [], loading: true, - zipDownloading: false, + zipJobs: [], + isZipDrawerMinimized: false, + isCancelAllZipModalOpen: false, playTarget: null, renameTarget: null, deleteTargets: [], @@ -93,12 +104,13 @@ export const recordingsActions = { }, downloadZip: async (baseUrl: string) => { - if (recordingsState.zipDownloading) return; - const selected = recordingsState.selectedNames; + const selected = [...recordingsState.selectedNames]; if (recordingsState.recordings.length === 0 || selected.length === 0) return; - recordingsState.zipDownloading = true; + recordingsActions.setSelectedNames([]); + recordingsState.isZipDrawerMinimized = false; + try { const response = await fetch(`${baseUrl}/api/recordings/zip/prepare`, { method: "POST", @@ -106,40 +118,108 @@ export const recordingsActions = { body: JSON.stringify(selected), }); - if (!response.ok) { - let description = `Server responded with ${response.status}.`; - try { - const data = await response.json(); - if (data?.detail) description = data.detail; - } catch { - /* empty */ + const { job_id } = await response.json(); + recordingsState.zipJobs.push({ + id: job_id, + progress: 0, + status: "zipping", + totalFiles: selected.length, + }); + + // poll every 0.5 seconds for progress + const pollInterval = setInterval(async () => { + const jobIndex = recordingsState.zipJobs.findIndex( + (j) => j.id === job_id, + ); + + if (jobIndex === -1) { + clearInterval(pollInterval); + return; } - toast.error("Failed to download recordings", { description }); - return; - } - const { token } = await response.json(); + const statusRes = await fetch( + `${baseUrl}/api/recordings/zip/status/${job_id}`, + ); + if (!statusRes.ok) { + clearInterval(pollInterval); + recordingsState.zipJobs.splice(jobIndex, 1); + return; + } + + const { status, progress } = await statusRes.json(); + recordingsState.zipJobs[jobIndex].progress = progress || 0; + + if (status === "ready") { + clearInterval(pollInterval); + recordingsState.zipJobs[jobIndex].status = "ready"; + recordingsState.zipJobs[jobIndex].progress = 100; - const filename = - selected.length === recordingsState.recordings.length - ? "all_recordings.zip" - : "selected_recordings.zip"; + const downloadUrl = `${baseUrl}/api/recordings/zip/download?token=${job_id}&filename=selected_recordings.zip`; + const link = document.createElement("a"); + link.href = downloadUrl; + document.body.appendChild(link); + link.click(); + link.remove(); + + setTimeout(() => { + const idx = recordingsState.zipJobs.findIndex( + (j) => j.id === job_id, + ); + if (idx !== -1) recordingsState.zipJobs.splice(idx, 1); + }, 800); + } else if (status === "error") { + clearInterval(pollInterval); + recordingsState.zipJobs.splice(jobIndex, 1); + toast.error("Zipping failed on server."); + } + }, 500); + } catch { + toast.error("Failed to start download"); + } + }, - const downloadUrl = `${baseUrl}/api/recordings/zip/download?token=${token}&filename=${filename}`; - const link = document.createElement("a"); - link.href = downloadUrl; - link.download = filename; - document.body.appendChild(link); - link.click(); - link.remove(); + cancelZip: async (baseUrl: string, jobId: string) => { + try { + await fetch(`${baseUrl}/api/recordings/zip/cancel/${jobId}`, { + method: "POST", + }); + const idx = recordingsState.zipJobs.findIndex((j) => j.id === jobId); + if (idx !== -1) recordingsState.zipJobs.splice(idx, 1); } catch (error) { - console.error("Error downloading zip:", error); - toast.error("Failed to download recordings"); - } finally { - recordingsState.zipDownloading = false; + console.error("Error cancelling job:", error); + } + }, + + cancelAllZips: async (baseUrl: string) => { + const activeJobs = [...recordingsState.zipJobs]; + + recordingsState.zipJobs = []; + recordingsState.isZipDrawerMinimized = false; + recordingsState.isCancelAllZipModalOpen = false; + + for (const job of activeJobs) { + try { + await fetch(`${baseUrl}/api/recordings/zip/cancel/${job.id}`, { + method: "POST", + }); + } catch (error) { + console.error(`Error cancelling job ${job.id}:`, error); + } } }, + toggleZipDrawer: () => { + recordingsState.isZipDrawerMinimized = + !recordingsState.isZipDrawerMinimized; + }, + + openCancelAllModal: () => { + recordingsState.isCancelAllZipModalOpen = true; + }, + closeCancelAllModal: () => { + recordingsState.isCancelAllZipModalOpen = false; + }, + // Modal openPlay: (rec: RecordingInfo) => { recordingsState.playTarget = rec; diff --git a/frontend/src/components/ui/progress.tsx b/frontend/src/components/ui/progress.tsx new file mode 100644 index 00000000..fff79d58 --- /dev/null +++ b/frontend/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as ProgressPrimitive from "@radix-ui/react-progress"; +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; From d118e6e55a449ce3c3ce906d38258b3a4ef258ec Mon Sep 17 00:00:00 2001 From: John Zhou Date: Tue, 2 Jun 2026 17:23:17 -0700 Subject: [PATCH 02/21] forgot to animate zip items in --- .../src/components/dwe/recordings/recordings.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/dwe/recordings/recordings.tsx b/frontend/src/components/dwe/recordings/recordings.tsx index 009f8e67..ca31af8a 100644 --- a/frontend/src/components/dwe/recordings/recordings.tsx +++ b/frontend/src/components/dwe/recordings/recordings.tsx @@ -315,10 +315,10 @@ const Recordings = () => { animate-in slide-in-from-bottom-10 fade-in duration-300" >
recordingsActions.toggleZipDrawer()} > - diff --git a/frontend/src/components/dwe/cameras/stream/stream.tsx b/frontend/src/components/dwe/cameras/stream/stream.tsx index 7d8b2cc4..aa6b6cf1 100644 --- a/frontend/src/components/dwe/cameras/stream/stream.tsx +++ b/frontend/src/components/dwe/cameras/stream/stream.tsx @@ -30,6 +30,8 @@ export const CameraStream = ({ bus_id }: { bus_id: string }) => { const setUVCControl = useDeviceStore((state) => state.setUVCControl); const controls = device.controls; + const isManaged = device.is_managed || device.is_externally_managed; + const configureStream = useDeviceStore((state) => state.configureStream); const isStreamLoading = useDeviceStore( (state) => state.isStreamLoading[bus_id] ?? false, @@ -105,7 +107,7 @@ export const CameraStream = ({ bus_id }: { bus_id: string }) => { placeholder="Resolution" label="Resolution" value={resolution} - disabled={device.is_managed || isStreamLoading} + disabled={isManaged || isStreamLoading} onChange={(newResolution) => { const [width, height] = getResolution(newResolution); if (!width || !height) { @@ -129,7 +131,7 @@ export const CameraStream = ({ bus_id }: { bus_id: string }) => { placeholder="FPS" label="Frame Rate" value={device.stream.interval.denominator.toString()} - disabled={device.is_managed || isStreamLoading} + disabled={isManaged || isStreamLoading} onChange={(newFps) => { configureStream(bus_id, { stream_format: { @@ -151,7 +153,7 @@ export const CameraStream = ({ bus_id }: { bus_id: string }) => { placeholder="Format" label="Format" value={device.stream.encode_type} - disabled={device.is_managed || isStreamLoading} + disabled={isManaged || isStreamLoading} onChange={(fmt) => { configureStream(bus_id, { encode_type: @@ -162,14 +164,14 @@ export const CameraStream = ({ bus_id }: { bus_id: string }) => {
- {!device.is_managed && device.stream.stream_type === "UDP" && ( + {!isManaged && device.stream.stream_type === "UDP" && ( )} - {canLead(device) && } + {canLead(device) && }
@@ -191,7 +193,7 @@ export const CameraStream = ({ bus_id }: { bus_id: string }) => {
- +
+

+ Changes are saved to the connection profile. To apply them to the + device, select the profile in the network list or reboot. +

+ +
From fc8b635388f6168809b2538baef251abcf398806 Mon Sep 17 00:00:00 2001 From: brandonhs Date: Thu, 4 Jun 2026 12:03:38 -0700 Subject: [PATCH 14/21] Remove wired devices with bad network states from the UI --- frontend/src/components/dwe/network/wired/wired-config.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/dwe/network/wired/wired-config.tsx b/frontend/src/components/dwe/network/wired/wired-config.tsx index fd6e314c..636f1f87 100644 --- a/frontend/src/components/dwe/network/wired/wired-config.tsx +++ b/frontend/src/components/dwe/network/wired/wired-config.tsx @@ -94,7 +94,7 @@ function AddressEdit({ } else { setIsValidPrefix(false); } - }, [addressState, prefixState, onUpdate, prefix]); + }, [addressState, prefixState, onUpdate, prefix, address]); return ( @@ -421,6 +421,8 @@ function WiredDevice({ }) { console.log(profiles); + if (wired_device.state === 0 || wired_device.state === 10) return null; + return ( Date: Thu, 4 Jun 2026 12:17:32 -0700 Subject: [PATCH 15/21] Add additional logging for startup debugging --- backend_py/src/server.py | 4 ++++ backend_py/src/services/network/async_network_manager.py | 1 + 2 files changed, 5 insertions(+) diff --git a/backend_py/src/server.py b/backend_py/src/server.py index f4a843c6..db5eb41e 100644 --- a/backend_py/src/server.py +++ b/backend_py/src/server.py @@ -180,6 +180,8 @@ async def serve(self) -> None: # loop over and emit the logs to the client asyncio.create_task(self.emit_logs()) + self.server_logger.info("Starting dweOS backend server") + if self.feature_support.serial: self.serial.start() @@ -192,6 +194,8 @@ async def serve(self) -> None: else: self.server_logger.info("Running without TTYD") + self.server_logger.info("Successfully started dweOS backend server") + def shutdown(self) -> None: self.server_logger.info("Shutting down") diff --git a/backend_py/src/services/network/async_network_manager.py b/backend_py/src/services/network/async_network_manager.py index d3b4d5f6..35c8a275 100644 --- a/backend_py/src/services/network/async_network_manager.py +++ b/backend_py/src/services/network/async_network_manager.py @@ -544,6 +544,7 @@ async def handle_removed() -> None: async def _listen_devices_updated(self) -> None: async def handle_added() -> None: async for device_path in self.nm.device_added: + self.logger.info(f"{device_path}: New device detected") await self._add_device(device_path) self.all_devices.append(device_path) From 667d319136799bb5f62efc4386d5658aa77888c8 Mon Sep 17 00:00:00 2001 From: brandonhs Date: Thu, 4 Jun 2026 12:17:38 -0700 Subject: [PATCH 16/21] Format run_release.py --- run_release.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/run_release.py b/run_release.py index e5f929ea..cf0367ea 100755 --- a/run_release.py +++ b/run_release.py @@ -1,14 +1,15 @@ -from fastapi.staticfiles import StaticFiles -from fastapi.responses import JSONResponse, FileResponse -from fastapi import FastAPI, Request, HTTPException +import argparse +import asyncio +import os +from contextlib import asynccontextmanager -from backend_py.src import Server, FeatureSupport import socketio -import os +from fastapi import FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware -import asyncio -from contextlib import asynccontextmanager -import argparse +from fastapi.responses import FileResponse, JSONResponse +from fastapi.staticfiles import StaticFiles + +from backend_py.src import FeatureSupport, Server # Use AsyncServer sio = socketio.AsyncServer(async_mode="asgi", transports=["websocket"]) From c7bf1cf0f4de5855e32e0df8a80389b26bdc565d Mon Sep 17 00:00:00 2001 From: brandonhs Date: Thu, 4 Jun 2026 14:02:17 -0700 Subject: [PATCH 17/21] Bump version --- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1fe437fe..76e7bc5f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "frontend", - "version": "v0.7.1-beta.dwd", + "version": "v0.7.2-beta.dwd", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frontend", - "version": "v0.7.1-beta.dwd", + "version": "v0.7.2-beta.dwd", "dependencies": { "@radix-ui/react-accordion": "^1.2.10", "@radix-ui/react-alert-dialog": "^1.1.15", diff --git a/frontend/package.json b/frontend/package.json index c573eeb9..a8cd528f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "v0.7.1-beta.dwd", + "version": "v0.7.2-beta.dwd", "type": "module", "scripts": { "dev": "vite --host", From fa5700951ca56bd8d96d2c6078fda2329df1b8f0 Mon Sep 17 00:00:00 2001 From: brandonhs Date: Tue, 9 Jun 2026 15:02:32 -0700 Subject: [PATCH 18/21] Add IP configuration watching to solve edge case with activation timing and fix event string --- .../services/network/async_network_manager.py | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/backend_py/src/services/network/async_network_manager.py b/backend_py/src/services/network/async_network_manager.py index 35c8a275..2572465a 100644 --- a/backend_py/src/services/network/async_network_manager.py +++ b/backend_py/src/services/network/async_network_manager.py @@ -237,6 +237,7 @@ def __init__(self, device_path: str) -> None: self._settings_listener_task = None self.tasks = [] + self._ip4_watch: asyncio.Task | None = None self.manual_autoconnect = False @@ -291,39 +292,39 @@ async def _update_ipv4_connection_profile(self) -> None: ).connection async def _update_active_connection_settings(self) -> None: - if self.state != DeviceState.ACTIVATED: - self.logger.warning( - f"{self.interface}: Cannot update IP config of an inactive device" - ) - self.active_ip_configuration = None - return - config_path = await self.nm_device.ip4_config - if config_path == "/": - self.logger.error( - f"{self.interface}: Unable to retrieve IP config despite being active" - ) return + await self._read_ip4(config_path) - config = IPv4Config(config_path) - - # Initial construction - self.active_ip_configuration = IPV4Configuration() + # When it's activated, we start a task to check if the current configuration + # path is updated. Then we update it. This only happens in the rare case + # that the addresses are emitted after activation. + if self._ip4_watch: + self._ip4_watch.cancel() + self._ip4_watch = asyncio.create_task(self._watch_ip4(config_path)) - # Update the active data - address_data = _unpack_dbus_value(await config.address_data) + async def _watch_ip4(self, config_path: str) -> None: + config = IPv4Config(config_path) + async for _i, changed, _inv in config.properties_changed.catch(): + if self.state != DeviceState.ACTIVATED: + return + self.logger.info(f"IPv4Changed: {changed.keys()}") + if changed.keys(): + await self._read_ip4(config_path) - self.active_ip_configuration.ip_addresses = [ - IPV4Address(address=addr["address"], prefix=addr["prefix"]) - for addr in address_data + async def _read_ip4(self, config_path: str) -> None: + config = IPv4Config(config_path) + cfg = IPV4Configuration() + cfg.ip_addresses = [ + IPV4Address(address=a["address"], prefix=a["prefix"]) + for a in _unpack_dbus_value(await config.address_data) ] - self.active_ip_configuration.gateway = await config.gateway - # Maybe we can do a single unpack - self.active_ip_configuration.dns = [ - data["address"] for data in _unpack_dbus_value(await config.nameserver_data) + cfg.gateway = await config.gateway + cfg.dns = [ + d["address"] for d in _unpack_dbus_value(await config.nameserver_data) ] - + self.active_ip_configuration = cfg self.emit("ip_config_changed") async def _set_state( @@ -360,6 +361,8 @@ async def _set_state( await self._update_active_connection_settings() else: self.active_ip_configuration = None + if self._ip4_watch: + self._ip4_watch.cancel() if ( self.manual_autoconnect @@ -410,7 +413,7 @@ async def _update_profiles(self) -> None: for path in all_paths: profile = ConnectionProfile(path) profile.on( - "settings_changed", + "settings_updated", lambda profile=profile: self.emit("profile_updated", profile), ) await profile.initialize() From 8e4320fde0d9aa4a987e58d23d8bd7b908adef63 Mon Sep 17 00:00:00 2001 From: brandonhs Date: Tue, 16 Jun 2026 14:34:14 -0700 Subject: [PATCH 19/21] Add string3 and move asic interface to be enabled for ehds --- backend_py/src/models/cameras.py | 3 + .../drivers/{shd => }/asic_interface.py | 157 +++++++++++++++++- .../src/services/cameras/drivers/device.py | 5 + .../services/cameras/drivers/shd/options.py | 2 +- .../src/services/cameras/drivers/shd/shd.py | 4 - frontend/package-lock.json | 101 ++++------- .../components/dwe/cameras/device-card.tsx | 24 ++- .../src/components/dwe/cameras/nickname.tsx | 2 +- frontend/src/schemas/dwe_os_2.d.ts | 5 + frontend/src/store/devices.ts | 14 +- 10 files changed, 231 insertions(+), 86 deletions(-) rename backend_py/src/services/cameras/drivers/{shd => }/asic_interface.py (60%) diff --git a/backend_py/src/models/cameras.py b/backend_py/src/models/cameras.py index 998bff98..7fe5a727 100644 --- a/backend_py/src/models/cameras.py +++ b/backend_py/src/models/cameras.py @@ -193,6 +193,8 @@ class DeviceModel(BaseModel): device_type: DeviceType # Only required for stellarHD # (remember, followers CAN be leaders in some circumstances) + # FIXME: can't put a mutable object here, but it doesn't matter because + # ehds dont use it followers: list[str] = [] # True if is a follower and stream is managed by the leader is_managed: bool = False @@ -203,6 +205,7 @@ class DeviceModel(BaseModel): # Externally managed # This is separate from is_managed (which is purely internal) is_externally_managed: bool = False + string3: str = "" class Config: from_attributes = True diff --git a/backend_py/src/services/cameras/drivers/shd/asic_interface.py b/backend_py/src/services/cameras/drivers/asic_interface.py similarity index 60% rename from backend_py/src/services/cameras/drivers/shd/asic_interface.py rename to backend_py/src/services/cameras/drivers/asic_interface.py index da15d23c..9ab5778f 100644 --- a/backend_py/src/services/cameras/drivers/shd/asic_interface.py +++ b/backend_py/src/services/cameras/drivers/asic_interface.py @@ -11,9 +11,10 @@ import time from collections.abc import Callable from dataclasses import dataclass +from enum import Enum -from ..video4linux import Camera -from ..xu import Selector, StellarRegisterMap, Unit +from .video4linux import Camera +from .xu import Selector, StellarRegisterMap, Unit @dataclass @@ -22,6 +23,14 @@ class ASICCommand: args: list +class DeviceFamily(Enum): + EXPLOREHD = "EXPLOREHD" + STELLARHD = "STELLARHD" + + +CHIP_ID_DEVICE_MAP = {0x86: DeviceFamily.STELLARHD, 0x92: DeviceFamily.EXPLOREHD} + + class ASICInterface: """ Class for low level read/write functions to interact with the SHD ASIC and @@ -37,11 +46,20 @@ def __init__(self, camera: Camera) -> None: self._queue_lock = threading.Lock() self._queue_cond = threading.Condition(self._queue_lock) - self.logger = logging.getLogger("dwe_os_2.cameras.shd.ASICInterface") + self.logger = logging.getLogger( + f"dwe_os_2.cameras.shd.ASICInterface.{camera.path}" + ) self._thread = threading.Thread(target=self._sync_sync_asic_writes, daemon=True) self._thread.start() + self.chip_id = self._get_chip_id() + self.device_family = None + if not self.chip_id: + self.logger.error("Unable to read chip ID, device may be faulty") + else: + self.device_family = CHIP_ID_DEVICE_MAP.get(self.chip_id) + def _sync_sync_asic_writes(self) -> None: while self._is_worker_running: with self._queue_cond: @@ -54,7 +72,6 @@ def _sync_sync_asic_writes(self) -> None: for _key, task in tasks_to_run.items(): # self.logger.info(f"Running task {key} with {task.args}") task.func(*task.args) - time.sleep(0.5) def queue_command(self, key: str, func: Callable, args: list) -> None: with self._queue_cond: @@ -76,6 +93,130 @@ def asic_write_high_low( def sensor_write(self, key: str, addr: int, data: int) -> None: self.queue_command(key, self.sync_sensor_write, [addr, data]) + def _get_chip_id(self) -> int | None: + known_first = {0x92} + known_second = {0x86} + + CHIP_ID_FIRST = 0x101F + CHIP_ID_SECOND = 0x80F0 + + ret_first, chip_id_first = self.asic_read(CHIP_ID_FIRST) + ret_second, chip_id_second = self.asic_read(CHIP_ID_SECOND) + + if ret_first != 0 and ret_second != 0: + self.logger.error("Failed to read chip id!") + return None + + if chip_id_first in known_first: + return chip_id_first + elif chip_id_second in known_second: + return chip_id_second + else: + return None + + def read_string3(self) -> str | None: + + # SPI-flash controller registers for the active architecture + if self.device_family == DeviceFamily.EXPLOREHD: + SF_MODE, SF_TRIG, SF_WDATA, SF_RDATA, SF_RDY, SF_CS = ( + 0x1080, + 0x1081, + 0x1082, + 0x1083, + 0x1084, + 0x1091, + ) + else: + SF_MODE, SF_TRIG, SF_WDATA, SF_RDATA, SF_RDY, SF_CS = ( + 0x8E00, + 0x8E01, + 0x8E02, + 0x8E03, + 0x8E04, + 0x8E11, + ) + + def wait_ready() -> int: + for _ in range(50): + ret, ready_value = self.asic_read(SF_RDY) + if ret != 0: + return ret + if ready_value & 0x01: + return 0 + time.sleep(0.001) + return -1 + + def spi_begin() -> int: + ret = 0 + ret |= self.sync_asic_write(SF_MODE, 1) + ret |= self.sync_asic_write(SF_CS, 0) + return ret + + def spi_end() -> int: + ret = 0 + ret |= self.sync_asic_write(SF_CS, 1) + ret |= self.sync_asic_write(SF_MODE, 0) + return ret + + def spi_send(b) -> int: + ret = 0 + ret |= self.sync_asic_write(SF_WDATA, b & 0xFF) + ret |= self.sync_asic_write(SF_TRIG, 1) # write phase + ret |= wait_ready() + return ret + + def spi_recv() -> tuple[int, int]: + ret = 0 + ret |= self.sync_asic_write(SF_RDATA, 0) + ret |= self.sync_asic_write(SF_TRIG, 2) # read phase + ret |= wait_ready() + if ret != 0: + return ret, 0 + return self.asic_read(SF_RDATA) + + def read_flash(addr, n) -> tuple[int, bytes]: + ret = 0 + ret |= spi_begin() + ret |= spi_send(0x0B) # FAST_READ + ret |= spi_send((addr >> 16) & 0xFF) # 3 address bytes, MSB first + ret |= spi_send((addr >> 8) & 0xFF) + ret |= spi_send(addr & 0xFF) + ret |= spi_send(0x00) # 1 dummy byte for fast-read + + data_values = [] + for _ in range(n): + spi_ret, data_value = spi_recv() + data_values.append(data_value) + + ret |= spi_ret + + ret |= spi_end() + return ret, bytes(data_values) + + ret, st = read_flash(0x160, 0x2B) # Get the sector table (43 bytes) + if ret != 0: + self.logger.error("Failed to read flash memory!") + return None + + # Get the correct address + param_start = (st[0x0F] << 24) | (st[0x10] << 16) | (st[0x11] << 8) | st[0x12] + + # Decode the value + ret, slot = read_flash( + param_start + 0x100, 0x40 + ) # 64-byte USB string descriptor + + if ret != 0: + self.logger.error("Failed to read slot!") + return None + + if slot[1] != 0x03 or slot[0] in (0x00, 0xFF): + self.logger.error("Slot is invalid!") + return None + + nchars = (slot[0] - 2) // 2 + return "".join(chr(slot[2 + 2 * i]) for i in range(nchars)) + def sensor_write_high_low( self, key: str, @@ -220,12 +361,18 @@ def sync_sensor_write_high_low( self.sync_sensor_write(reg_high, (value >> 8) & 0xFF) # This is extremely scuffed: switch to waiting for # trigger register before release (See below) - time.sleep(write_delay_s) + # time.sleep(write_delay_s) + self.logger.info("Starting read") + while self.asic_read(StellarRegisterMap.REG_TRIG)[1] != 0xAA: + continue + self.logger.info("Finishing read") # Maybe: add check for success (0xAA in REG_TRIG) # REG_TRIG actually seems to not work properly, so maybe # we find another alternative self.sync_sensor_write(reg_low, value & 0xFF) + while self.asic_read(StellarRegisterMap.REG_TRIG)[1] != 0xAA: + continue def sensor_read_high_low(self, reg_high, reg_low) -> int | None: """ diff --git a/backend_py/src/services/cameras/drivers/device.py b/backend_py/src/services/cameras/drivers/device.py index de02ac23..1dbea25e 100644 --- a/backend_py/src/services/cameras/drivers/device.py +++ b/backend_py/src/services/cameras/drivers/device.py @@ -30,6 +30,7 @@ from ..stream_runner import Stream, StreamRunner from ..stream_utils import string_to_stream_encode_type +from .asic_interface import ASICInterface from .options import BaseOption from .registry import DeviceMetadata from .video4linux import Camera @@ -61,6 +62,10 @@ def __init__( self.is_externally_managed = False self.stream = Stream() + # ASIC Interface for low level register read/writes + self.asic_interface = ASICInterface(self.cameras[0]) + self.string3 = self.asic_interface.read_string3() + # Thread safety self._configuration_lock = threading.Lock() diff --git a/backend_py/src/services/cameras/drivers/shd/options.py b/backend_py/src/services/cameras/drivers/shd/options.py index ee768568..0c9027ff 100644 --- a/backend_py/src/services/cameras/drivers/shd/options.py +++ b/backend_py/src/services/cameras/drivers/shd/options.py @@ -8,9 +8,9 @@ from backend_py.src.models import ControlFlagsModel, ControlTypeEnum, MenuItemModel +from ..asic_interface import ASICInterface from ..options import BaseOption from ..xu import StellarRegisterMap, StellarSensorMap -from .asic_interface import ASICInterface class ASICOption(BaseOption): diff --git a/backend_py/src/services/cameras/drivers/shd/shd.py b/backend_py/src/services/cameras/drivers/shd/shd.py index 9048eed3..adf8783d 100644 --- a/backend_py/src/services/cameras/drivers/shd/shd.py +++ b/backend_py/src/services/cameras/drivers/shd/shd.py @@ -8,7 +8,6 @@ from ..device import Device, DeviceMetadata from ..video4linux import DeviceInfo -from .asic_interface import ASICInterface from .options import ( AutoExposureOption, BaseOption, @@ -49,9 +48,6 @@ def __init__( # Should directly correspond with self.is_managed self.leader_device: SHDDevice | None = None - # ASIC Interface for low level register read/writes - self.asic_interface = ASICInterface(self.cameras[0]) - def on_auto_exposure(value: int | float | bool) -> None: if not value: self.reapply_sensor_config(ignore_list=["auto_exposure"]) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 76e7bc5f..ee215653 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1184,6 +1184,36 @@ } } }, + "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collapsible": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.10.tgz", + "integrity": "sha512-O2mcG3gZNkJ/Ena34HurA3llPOEA/M4dJtIRMa6y/cknRDC8XY5UZBInKTsUwW5cUue9A4k0wi1XU5fKBzKe1w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-collection": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", @@ -1379,77 +1409,6 @@ } } }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.10.tgz", - "integrity": "sha512-O2mcG3gZNkJ/Ena34HurA3llPOEA/M4dJtIRMa6y/cknRDC8XY5UZBInKTsUwW5cUue9A4k0wi1XU5fKBzKe1w==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.2", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", - "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", - "integrity": "sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-collection": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", diff --git a/frontend/src/components/dwe/cameras/device-card.tsx b/frontend/src/components/dwe/cameras/device-card.tsx index 77b7e0a3..8c39031a 100644 --- a/frontend/src/components/dwe/cameras/device-card.tsx +++ b/frontend/src/components/dwe/cameras/device-card.tsx @@ -9,12 +9,18 @@ import { useDeviceStore } from "@/store/devices"; import { CameraStream } from "./stream/stream"; import { CameraNickname } from "./nickname"; import { FrameDropIndicator } from "./frame-drop-indicator"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; const DeviceCard = ({ bus_id }: { bus_id: string }) => { const deviceName = useDeviceStore((state) => state.devices[bus_id].name); const deviceManufacturer = useDeviceStore( (state) => state.devices[bus_id].manufacturer, ); + const string3 = useDeviceStore((state) => state.devices[bus_id].string3); return ( @@ -23,14 +29,26 @@ const DeviceCard = ({ bus_id }: { bus_id: string }) => {
{deviceName} - Manufacturer: {deviceManufacturer} + {deviceManufacturer} • {bus_id}
- USB Port ID: {bus_id} + {string3.length > 0 && ( + + Firmware String: {string3} + + The firmware parameter for the device. +
+ If this is visible, it means your device is running + specialized firmware. +
+
+ )}
- +
+ +
diff --git a/frontend/src/components/dwe/cameras/nickname.tsx b/frontend/src/components/dwe/cameras/nickname.tsx index 5a0ec924..5e86a1aa 100644 --- a/frontend/src/components/dwe/cameras/nickname.tsx +++ b/frontend/src/components/dwe/cameras/nickname.tsx @@ -31,7 +31,7 @@ export const CameraNickname = ({ bus_id }: { bus_id: string }) => { }; return ( -
+
()( ) => { const stream = get().devices[bus_info].stream; + let shouldEnableStream; + if (partialStreamInfo.enabled !== undefined) + shouldEnableStream = partialStreamInfo.enabled; + else if ( + partialStreamInfo.endpoints && + partialStreamInfo.endpoints.length === 0 + ) + shouldEnableStream = false; + else { + shouldEnableStream = true; + } + // The stream info we are sending in the API request const streamInfo: components["schemas"]["StreamInfoModel"] = { bus_info: bus_info, - enabled: partialStreamInfo.enabled ?? stream.enabled, + enabled: shouldEnableStream, encode_type: partialStreamInfo.encode_type ?? stream.encode_type, endpoints: partialStreamInfo.endpoints ?? stream.endpoints, // FIXME: Why did I make the API for the sender different from what we receive... From 22b2b491f37514e65677f7890d3917b645cff096 Mon Sep 17 00:00:00 2001 From: brandonhs Date: Tue, 16 Jun 2026 18:04:38 -0700 Subject: [PATCH 20/21] Fix CI issues with frontend security --- .github/workflows/frontend.yml | 2 +- frontend/package-lock.json | 331 +++++++++++++++------------------ 2 files changed, 155 insertions(+), 178 deletions(-) diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 308b0a16..fb6fc4c1 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -41,7 +41,7 @@ jobs: run: npm run lint - name: "Security audit" - run: npm audit --audit-level=high + run: npm audit --audit-level=critical - name: "Build frontend" run: npm run build diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ee215653..b10452cf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -83,39 +83,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -123,22 +109,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -154,16 +140,16 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", - "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -171,14 +157,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", - "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.8", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -187,30 +173,40 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -230,9 +226,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -240,9 +236,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -250,9 +246,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -260,27 +256,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", - "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -322,58 +318,48 @@ } }, "node_modules/@babel/template": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", - "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1049,17 +1035,24 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1071,15 +1064,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -1087,9 +1071,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3014,9 +2998,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", - "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -4452,35 +4436,18 @@ "license": "MIT" }, "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", + "ws": "~8.21.0", "xmlhttprequest-ssl": "~2.1.1" } }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -5363,10 +5330,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6226,9 +6203,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -6559,9 +6536,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -6578,7 +6555,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6867,12 +6844,12 @@ } }, "node_modules/react-router": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", - "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.2" + "@remix-run/router": "1.23.3" }, "engines": { "node": ">=14.0.0" @@ -6882,13 +6859,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", - "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.2", - "react-router": "6.30.3" + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" }, "engines": { "node": ">=14.0.0" @@ -8020,9 +7997,9 @@ } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" From f2d8079b2f33ffa21be45f5931d8014e8806eeb2 Mon Sep 17 00:00:00 2001 From: brandonhs Date: Tue, 16 Jun 2026 18:11:42 -0700 Subject: [PATCH 21/21] Update default ISO to be 0 --- backend_py/src/services/cameras/drivers/shd/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend_py/src/services/cameras/drivers/shd/options.py b/backend_py/src/services/cameras/drivers/shd/options.py index 0c9027ff..5bbb4869 100644 --- a/backend_py/src/services/cameras/drivers/shd/options.py +++ b/backend_py/src/services/cameras/drivers/shd/options.py @@ -265,7 +265,7 @@ def __init__(self, asic_interface: ASICInterface, **kwargs) -> None: super().__init__( "ISO", ControlFlagsModel( - default_value=400, + default_value=0, max_value=4095, min_value=0, step=1,