From 4d89bd3309f2af24c0f0596d0a5b91838f398510 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:59:57 -0500 Subject: [PATCH] reveal camera paths by masked value instead of position After deleting or reordering inputs in the camera ffmpeg form, the path widget looked up raw_paths by index and revealed another input's real path, which a save then wrote with the wrong roles. The widget now matches the saved path whose mask equals the field's value, the same rule the backend uses on save, and doesn't reveal when there is no single match. --- .../settings/camera-ffmpeg-streams.spec.ts | 39 ++++++++++++++++++- .../theme/widgets/CameraPathWidget.tsx | 21 +++++++--- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts b/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts index 38b0f5c4a6..d343a3b3ee 100644 --- a/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts +++ b/web/e2e/specs/settings/camera-ffmpeg-streams.spec.ts @@ -32,7 +32,11 @@ type CameraInput = { input_args?: string; }; -async function installRoutes(page: Page, frontDoorInputs: CameraInput[]) { +async function installRoutes( + page: Page, + frontDoorInputs: CameraInput[], + rawInputs: CameraInput[] = frontDoorInputs, +) { const config = configFactory({ go2rtc: { streams: GO2RTC_STREAMS }, cameras: { @@ -56,7 +60,7 @@ async function installRoutes(page: Page, frontDoorInputs: CameraInput[]) { await page.route("**/api/config/raw_paths", (route) => route.fulfill({ json: { - cameras: { front_door: { ffmpeg: { inputs: frontDoorInputs } } }, + cameras: { front_door: { ffmpeg: { inputs: rawInputs } } }, go2rtc: { streams: GO2RTC_STREAMS }, }, }), @@ -200,4 +204,35 @@ test.describe("camera ffmpeg input source toggle @medium", () => { payload?.config_data?.cameras?.front_door?.ffmpeg?.inputs?.[0]; expect(input?.input_args).not.toBe("preset-rtsp-restream"); }); + + test("a masked path reveals its own saved path after an earlier input is deleted", async ({ + frigateApp, + }) => { + const main = "rtsp://admin:mainpw@10.0.0.1:554/main"; + const sub = "rtsp://admin:subpw@10.0.0.1:554/sub"; + await installRoutes( + frigateApp.page, + [ + { path: "rtsp://*:*@10.0.0.1:554/main", roles: ["record"] }, + { path: "rtsp://*:*@10.0.0.1:554/sub", roles: ["detect"] }, + ], + [ + { path: main, roles: ["record"] }, + { path: sub, roles: ["detect"] }, + ], + ); + await frigateApp.goto("/settings?page=cameraFfmpeg&camera=front_door"); + + const paths = frigateApp.page.getByRole("textbox", { name: "Input path" }); + await expect(paths).toHaveCount(2); + + await frigateApp.page + .getByRole("button", { name: "Delete" }) + .first() + .click(); + await expect(paths).toHaveCount(1); + + await paths.focus(); + await expect(paths).toHaveValue(sub); + }); }); diff --git a/web/src/components/config-form/theme/widgets/CameraPathWidget.tsx b/web/src/components/config-form/theme/widgets/CameraPathWidget.tsx index 03f6abb9d9..4e446e8595 100644 --- a/web/src/components/config-form/theme/widgets/CameraPathWidget.tsx +++ b/web/src/components/config-form/theme/widgets/CameraPathWidget.tsx @@ -66,17 +66,26 @@ export function CameraPathWidget(props: WidgetProps) { shouldFetchRawPaths ? "config/raw_paths" : null, ); + const rawValue = typeof value === "string" ? value : ""; + + // Match by masked value, as the backend restores on save, so deleting or + // reordering inputs never reveals another input's path const rawPath = useMemo(() => { - if (!cameraName || inputIndex === undefined) { + if (!cameraName || !isMaskedPath(rawValue)) { return undefined; } - const path = - rawPaths?.cameras?.[cameraName]?.ffmpeg?.inputs?.[inputIndex]?.path; - return typeof path === "string" ? path : undefined; - }, [cameraName, inputIndex, rawPaths]); + const matches = new Set( + (rawPaths?.cameras?.[cameraName]?.ffmpeg?.inputs ?? []) + .map((input) => input.path) + .filter( + (path): path is string => + typeof path === "string" && maskCredentials(path) === rawValue, + ), + ); + return matches.size === 1 ? [...matches][0] : undefined; + }, [cameraName, rawPaths, rawValue]); - const rawValue = typeof value === "string" ? value : ""; const resolvedValue = isMaskedPath(rawValue) && rawPath ? rawPath : (rawValue ?? ""); const canReveal =