diff --git a/web/src/components/config-form/section-configs/objects.ts b/web/src/components/config-form/section-configs/objects.ts index 48b764df1a..96b6844cc2 100644 --- a/web/src/components/config-form/section-configs/objects.ts +++ b/web/src/components/config-form/section-configs/objects.ts @@ -1,4 +1,5 @@ import type { HiddenFieldContext } from "@/types/configForm"; +import { getEffectiveAttributeLabels } from "@/utils/configUtil"; import type { SectionConfigOverrides } from "./types"; // Attribute labels (face, license_plate, Frigate+ couriers like DHL/Amazon, @@ -27,7 +28,7 @@ const hideAttributeFilters = ({ fullConfig.objects?.track ?? []; - return (fullConfig.model?.all_attributes ?? []) + return getEffectiveAttributeLabels(fullConfig, fullCameraConfig, level) .filter((attr) => !track.includes(attr)) .map((attr) => `filters.${attr}`); }; diff --git a/web/src/components/config-form/sections/section-special-cases.ts b/web/src/components/config-form/sections/section-special-cases.ts index 506b2e20ad..256c275ebb 100644 --- a/web/src/components/config-form/sections/section-special-cases.ts +++ b/web/src/components/config-form/sections/section-special-cases.ts @@ -10,6 +10,7 @@ import { RJSFSchema } from "@rjsf/utils"; import { applySchemaDefaults } from "@/lib/config-schema"; import { isJsonObject } from "@/lib/utils"; import { HiddenFieldContext, JsonObject, JsonValue } from "@/types/configForm"; +import { getEffectiveAttributeLabels } from "@/utils/configUtil"; /** * Sections that require special handling at the global level. @@ -149,7 +150,11 @@ function modifyObjectsSchema( ): RJSFSchema { if (!ctx) return schema; - const allAttributes = ctx.fullConfig.model?.all_attributes ?? []; + const allAttributes = getEffectiveAttributeLabels( + ctx.fullConfig, + ctx.fullCameraConfig, + ctx.level, + ); // Resolve effective track at this scope, falling back through camera // config then global config (matches hideAttributeFilters in objects.ts). diff --git a/web/src/components/config-form/theme/utils/i18n.ts b/web/src/components/config-form/theme/utils/i18n.ts index e0e76eaefa..a5f7ea1527 100644 --- a/web/src/components/config-form/theme/utils/i18n.ts +++ b/web/src/components/config-form/theme/utils/i18n.ts @@ -6,6 +6,7 @@ */ import type { ConfigFormContext } from "@/types/configForm"; +import { getEffectiveAttributeLabels } from "@/utils/configUtil"; const isRecord = (value: unknown): value is Record => typeof value === "object" && value !== null; @@ -80,7 +81,11 @@ export function buildTranslationPath( const filtersIndex = stringSegments.indexOf("filters"); if (filtersIndex !== -1 && stringSegments.length > filtersIndex + 2) { const filterKey = stringSegments[filtersIndex + 1]; - const allAttributes = formContext?.fullConfig?.model?.all_attributes ?? []; + const allAttributes = getEffectiveAttributeLabels( + formContext?.fullConfig, + formContext?.fullCameraConfig, + formContext?.level, + ); const sectionWord = allAttributes.includes(filterKey) ? "filters_attribute" : "filters"; diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index e69ae86ee7..4b6ffefb71 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -19,7 +19,7 @@ import { sanitizeOverridesForSection, } from "@/components/config-form/sections/section-special-cases"; import type { RJSFSchema } from "@rjsf/utils"; -import type { FrigateConfig } from "@/types/frigateConfig"; +import type { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; import type { ConfigSectionData, HiddenFieldContext, @@ -749,6 +749,26 @@ export function getSectionConfig( return mergeSectionConfig(entry.base, overrides); } +/** + * Resolve the effective attribute label set at a given scope. At camera + * (and replay) scope on a dedicated LPR camera (`camera.type === "lpr"`), + * `license_plate` is treated as a regular tracked object — not an + * attribute — to match the backend's per-camera carve-out in + * `frigate/video/detect.py`. Returns the full attribute list at global + * scope and for non-LPR cameras. + */ +export function getEffectiveAttributeLabels( + fullConfig: FrigateConfig | undefined, + fullCameraConfig: CameraConfig | undefined, + level: "global" | "camera" | "replay" | undefined, +): string[] { + const all = fullConfig?.model?.all_attributes ?? []; + if (level !== "global" && fullCameraConfig?.type === "lpr") { + return all.filter((attr) => attr !== "license_plate"); + } + return all; +} + /** * Build a `HiddenFieldContext` for the common case where a callsite has * `config`, an optional `cameraName`, and a level, but no per-section