Miscellaneous fixes (#24878)
CI / AMD64 Build (push) Waiting to run
CI / AMD64 Smoke Test (push) Blocked by required conditions
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions

* don't calibrate or skip capability checks when autotracking is enabled at runtime

Enabling autotracking from the UI on a camera that had been disabled for missing FOV relative movement or MoveStatus skipped both checks because ONVIF was already initialized, then ran calibration on the object processing thread and hung waiting for a motor stop that never came. Setup now runs the capability checks every time, only calibrates at startup, stops tracking the triggering object when setup disables the camera, and publishes the OFF state so the toggle reflects it.

* only show settings reset buttons when the reset can succeed

Reset to Global and Reset to Default delete the section from config.yml, and the delete raised a KeyError (HTTP 500) when the key wasn't in the file. Camera live sections always read as overridden because the backend fills in live.streams per camera, and the global Reset to Default button showed for every section, including ones the user never wrote. Global reset now only shows when the section differs from its schema defaults, ignoring fields the backend resolves at runtime (ffmpeg and record export hwaccel_args, audio filters). Camera live overrides skip streams, ffmpeg overrides skip inputs, and reset is hidden for camera ffmpeg and detection models, where deleting the section can't produce a valid config. Birdseye modes uses a plain default so the schema carries it.

* Added an e2e spec covering reset visibility for default and customized global sections, the runtime-resolved fields, camera live overrides, and camera ffmpeg
This commit is contained in:
Josh Hawkins
2026-11-04 16:23:27 -07:00
committed by GitHub
parent 18a2eccab1
commit 1cb5ec4107
11 changed files with 196 additions and 37 deletions
@@ -0,0 +1,126 @@
/**
* Section reset button visibility -- MEDIUM tier.
*
* Reset deletes the section from config.yml, which fails when the key isn't
* in the file. The button only shows when the section's values must have
* come from the YAML.
*/
import { readFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { test, expect, type FrigateApp } from "../../fixtures/frigate-test";
import type { Page } from "@playwright/test";
import {
configFactory,
type DeepPartial,
} from "../../fixtures/mock-data/config";
const __dirname = dirname(fileURLToPath(import.meta.url));
const CONFIG_SCHEMA = JSON.parse(
readFileSync(
resolve(__dirname, "../../fixtures/mock-data/config-schema.json"),
"utf-8",
),
);
async function openSettings(
app: FrigateApp,
url: string,
overrides: DeepPartial<ReturnType<typeof configFactory>> = {},
) {
const { page } = app;
const config = configFactory(overrides);
await page.route("**/api/config/schema.json", (route) =>
route.fulfill({ json: CONFIG_SCHEMA }),
);
await page.route("**/api/config", (route) => route.fulfill({ json: config }));
await page.route("**/api/config/raw_paths", (route) =>
route.fulfill({ json: { cameras: {}, go2rtc: { streams: {} } } }),
);
await app.goto(url);
}
const resetToDefault = (page: Page) =>
page.getByRole("button", { name: "Reset to Default" });
const resetToGlobal = (page: Page) =>
page.getByRole("button", { name: "Reset to Global" });
test.describe("section reset visibility @medium", () => {
test("global section at its defaults hides reset", async ({ frigateApp }) => {
await openSettings(frigateApp, "/settings?page=globalRecording");
await expect(
frigateApp.page.getByText("Retention", { exact: false }).first(),
).toBeVisible();
await expect(resetToDefault(frigateApp.page)).toHaveCount(0);
});
test("global section with a changed value shows reset", async ({
frigateApp,
}) => {
await openSettings(frigateApp, "/settings?page=globalRecording", {
record: { expire_interval: 30 },
});
await expect(resetToDefault(frigateApp.page)).toBeVisible();
});
test("global ffmpeg with only resolved hwaccel args hides reset", async ({
frigateApp,
}) => {
await openSettings(frigateApp, "/settings?page=globalFfmpeg");
await expect(
frigateApp.page
.getByText("Hardware acceleration", { exact: false })
.first(),
).toBeVisible();
await expect(resetToDefault(frigateApp.page)).toHaveCount(0);
});
test("camera live with only backend streams hides reset", async ({
frigateApp,
}) => {
await openSettings(
frigateApp,
"/settings?page=cameraLivePlayback&camera=front_door",
);
await expect(
frigateApp.page.getByRole("switch", {
name: "Enable transcoded streams",
}),
).toBeVisible();
await expect(resetToGlobal(frigateApp.page)).toHaveCount(0);
});
test("camera live with a quality override shows reset", async ({
frigateApp,
}) => {
await openSettings(
frigateApp,
"/settings?page=cameraLivePlayback&camera=front_door",
{ cameras: { front_door: { live: { quality: 4 } } } },
);
await expect(resetToGlobal(frigateApp.page)).toBeVisible();
});
test("camera ffmpeg never shows reset", async ({ frigateApp }) => {
await openSettings(
frigateApp,
"/settings?page=cameraFfmpeg&camera=front_door",
{ cameras: { front_door: { ffmpeg: { output_args: { record: "x" } } } } },
);
await expect(
frigateApp.page
.getByText("Hardware acceleration", { exact: false })
.first(),
).toBeVisible();
await expect(resetToGlobal(frigateApp.page)).toHaveCount(0);
});
});