Debug replay fixes (#23276)

* filter replay camera from camera selectors

* add face rec and lpr to replay configuration sheet

* add missing config topic subscriptions in embeddings maintainer

* pop replay camera from config object when stopping
This commit is contained in:
Josh Hawkins 2026-05-21 09:12:53 -05:00 committed by GitHub
parent 01c82d6921
commit 555ef89800
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 119 additions and 32 deletions

View File

@ -169,6 +169,7 @@ class DebugReplayManager:
CameraConfigUpdateTopic(CameraConfigUpdateEnum.remove, replay_name),
frigate_config.cameras[replay_name],
)
frigate_config.cameras.pop(replay_name, None)
if replay_name is not None:
self._cleanup_db(replay_name)

View File

@ -98,10 +98,17 @@ class EmbeddingMaintainer(threading.Thread):
[
CameraConfigUpdateEnum.add,
CameraConfigUpdateEnum.remove,
CameraConfigUpdateEnum.detect,
CameraConfigUpdateEnum.face_recognition,
CameraConfigUpdateEnum.ffmpeg,
CameraConfigUpdateEnum.lpr,
CameraConfigUpdateEnum.motion,
CameraConfigUpdateEnum.objects,
CameraConfigUpdateEnum.object_genai,
CameraConfigUpdateEnum.review,
CameraConfigUpdateEnum.review_genai,
CameraConfigUpdateEnum.semantic_search,
CameraConfigUpdateEnum.zones,
],
)
self.enrichment_config_subscriber = ConfigSubscriber("config/")

View File

@ -14,6 +14,7 @@ import Konva from "konva";
import { useResizeObserver } from "@/hooks/resize-observer";
import { useApiHost } from "@/api";
import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
import { isReplayCamera } from "@/utils/cameraUtil";
import Heading from "@/components/ui/heading";
import { isMobile } from "react-device-detect";
import { cn } from "@/lib/utils";
@ -67,6 +68,7 @@ export default function Step2StateArea({
([name, cam]) =>
cam.enabled &&
cam.enabled_in_config &&
!isReplayCamera(name) &&
!selectedCameraNames.includes(name),
)
.map(([name]) => ({

View File

@ -57,6 +57,7 @@ import isEqual from "lodash/isEqual";
import set from "lodash/set";
import type { ConfigSectionData, JsonObject } from "@/types/configForm";
import { sanitizeSectionData } from "@/utils/configUtil";
import { isReplayCamera } from "@/utils/cameraUtil";
import type { SectionRendererProps } from "./registry";
const NOTIFICATION_SERVICE_WORKER = "/notifications-worker.js";
@ -94,7 +95,7 @@ export default function NotificationsSettingsExtras({
return Object.values(config.cameras)
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order)
.filter((c) => c.enabled_in_config);
.filter((c) => c.enabled_in_config && !isReplayCamera(c.name));
}, [config]);
const notificationCameras = useMemo(() => {
@ -106,6 +107,7 @@ export default function NotificationsSettingsExtras({
.filter(
(conf) =>
conf.enabled_in_config &&
!isReplayCamera(conf.name) &&
conf.notifications &&
conf.notifications.enabled_in_config,
)
@ -359,6 +361,7 @@ export default function NotificationsSettingsExtras({
Object.values(config.cameras).some(
(c) =>
c.enabled_in_config &&
!isReplayCamera(c.name) &&
c.notifications &&
c.notifications.enabled_in_config,
),

View File

@ -26,6 +26,7 @@ import {
import { useTranslation } from "react-i18next";
import { FrigateConfig } from "@/types/frigateConfig";
import { CameraNameLabel } from "../camera/FriendlyNameLabel";
import { isReplayCamera } from "@/utils/cameraUtil";
import { isDesktop, isMobile } from "react-device-detect";
import { cn } from "@/lib/utils";
import {
@ -52,7 +53,9 @@ export default function CreateRoleDialog({
const { t } = useTranslation(["views/settings"]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const cameras = Object.keys(config.cameras || {});
const cameras = Object.keys(config.cameras || {}).filter(
(name) => !isReplayCamera(name),
);
const existingRoles = Object.keys(config.auth?.roles || {});

View File

@ -25,6 +25,7 @@ import {
import { Trans, useTranslation } from "react-i18next";
import { FrigateConfig } from "@/types/frigateConfig";
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
import { isReplayCamera } from "@/utils/cameraUtil";
type EditRoleCamerasOverlayProps = {
show: boolean;
@ -46,7 +47,9 @@ export default function EditRoleCamerasDialog({
const { t } = useTranslation(["views/settings"]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const cameras = Object.keys(config.cameras || {});
const cameras = Object.keys(config.cameras || {}).filter(
(name) => !isReplayCamera(name),
);
const formSchema = z.object({
cameras: z

View File

@ -54,6 +54,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { Textarea } from "../ui/textarea";
import { useNavigate } from "react-router-dom";
import { useIsAdmin } from "@/hooks/use-is-admin";
import { isReplayCamera } from "@/utils/cameraUtil";
const EXPORT_OPTIONS = [
"1",
@ -448,7 +449,9 @@ export function ExportContent({
);
const cameraActivities = useMemo<CameraActivity[]>(() => {
const allCameraIds = Object.keys(config?.cameras ?? {});
const allCameraIds = Object.keys(config?.cameras ?? {}).filter(
(name) => !isReplayCamera(name),
);
const byCamera = new Map<string, Event[]>();
events?.forEach((event) => {

View File

@ -13,6 +13,7 @@ import {
} from "@/components/ui/select";
import { Card, CardContent } from "@/components/ui/card";
import { formatUnixTimestampToDateTime } from "@/utils/dateUtil";
import { isReplayCamera } from "@/utils/cameraUtil";
import { useTimezone } from "@/hooks/use-date-utils";
import { Button } from "@/components/ui/button";
import { LuX } from "react-icons/lu";
@ -36,11 +37,16 @@ export default function ObjectPathPlotter() {
const [currentPage, setCurrentPage] = useState(1);
const eventsPerPage = 20;
const cameraNames = useMemo(() => {
if (!config) return [];
return Object.keys(config.cameras).filter((name) => !isReplayCamera(name));
}, [config]);
useEffect(() => {
if (config && !selectedCamera) {
setSelectedCamera(Object.keys(config.cameras)[0]);
if (cameraNames.length > 0 && !selectedCamera) {
setSelectedCamera(cameraNames[0]);
}
}, [config, selectedCamera]);
}, [cameraNames, selectedCamera]);
const searchQuery = useMemo(() => {
if (!selectedCamera) return null;
@ -143,12 +149,11 @@ export default function ObjectPathPlotter() {
<SelectValue placeholder="Select camera" />
</SelectTrigger>
<SelectContent>
{config &&
Object.keys(config.cameras).map((cameraName) => (
<SelectItem key={cameraName} value={cameraName}>
{cameraName}
</SelectItem>
))}
{cameraNames.map((cameraName) => (
<SelectItem key={cameraName} value={cameraName}>
{cameraName}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={timeRange} onValueChange={setTimeRange}>

View File

@ -18,6 +18,7 @@ import {
} from "@/utils/configUtil";
import { extractSectionSchema } from "@/hooks/use-config-schema";
import { applySchemaDefaults } from "@/lib/config-schema";
import { isReplayCamera } from "@/utils/cameraUtil";
const INTERNAL_FIELD_SUFFIXES = ["enabled_in_config", "raw_mask"];
@ -602,9 +603,13 @@ function getEffectiveGlobalBaseline(
return normalizeConfigValue(defaults as JsonValue);
}
}
const cameraSectionValues = Object.keys(config.cameras ?? {}).map((name) =>
normalizeConfigValue(getBaseCameraSectionValue(config, name, sectionPath)),
);
const cameraSectionValues = Object.keys(config.cameras ?? {})
.filter((name) => !isReplayCamera(name))
.map((name) =>
normalizeConfigValue(
getBaseCameraSectionValue(config, name, sectionPath),
),
);
return deriveSyntheticGlobalValue(cameraSectionValues, compareFields);
}
@ -684,7 +689,9 @@ export function useCamerasOverridingSection(
const sectionMeta = OVERRIDABLE_SECTIONS.find((s) => s.key === sectionPath);
const compareFields = sectionMeta?.compareFields;
const cameraNames = Object.keys(config.cameras);
const cameraNames = Object.keys(config.cameras).filter(
(name) => !isReplayCamera(name),
);
const cameraSectionValues = cameraNames.map((name) =>
normalizeConfigValue(
getBaseCameraSectionValue(config, name, sectionPath),

View File

@ -1,6 +1,7 @@
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { isReplayCamera } from "@/utils/cameraUtil";
/**
* Returns true if the current user has access to all cameras.
@ -16,7 +17,7 @@ export function useHasFullCameraAccess() {
if (!config?.cameras) return false;
const enabledCameraNames = Object.entries(config.cameras)
.filter(([, cam]) => cam.enabled_in_config)
.filter(([name, cam]) => cam.enabled_in_config && !isReplayCamera(name))
.map(([name]) => name);
return (

View File

@ -637,7 +637,7 @@ export default function Events() {
}
setStartTime(recording.startTime);
const allCameras = reviewFilter?.cameras ?? Object.keys(config.cameras);
const allCameras = reviewFilter?.cameras ?? allowedCameras;
return {
camera: recording.camera,

View File

@ -378,6 +378,34 @@ export default function Replay() {
showTitle
showOverrideIndicator={false}
/>
{config?.face_recognition?.enabled && (
<ConfigSectionTemplate
sectionKey="face_recognition"
level="replay"
cameraName={status.replay_camera ?? undefined}
skipSave
noStickyButtons
requiresRestart={false}
collapsible
defaultCollapsed={false}
showTitle
showOverrideIndicator={false}
/>
)}
{config?.lpr?.enabled && (
<ConfigSectionTemplate
sectionKey="lpr"
level="replay"
cameraName={status.replay_camera ?? undefined}
skipSave
noStickyButtons
requiresRestart={false}
collapsible
defaultCollapsed={false}
showTitle
showOverrideIndicator={false}
/>
)}
</div>
)}
</>

View File

@ -100,6 +100,7 @@ import {
} from "@/utils/configUtil";
import type { ProfileState, ProfilesApiResponse } from "@/types/profile";
import { getProfileColor } from "@/utils/profileColors";
import { isReplayCamera } from "@/utils/cameraUtil";
import { ProfileSectionDropdown } from "@/components/settings/ProfileSectionDropdown";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import RestartDialog from "@/components/overlay/dialog/RestartDialog";
@ -661,7 +662,12 @@ export default function Settings() {
}
return Object.values(config.cameras)
.filter((conf) => conf.ui.dashboard && conf.enabled_in_config)
.filter(
(conf) =>
conf.ui.dashboard &&
conf.enabled_in_config &&
!isReplayCamera(conf.name),
)
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config]);

View File

@ -32,6 +32,7 @@ import {
ZoomLevel,
} from "@/types/review";
import { getChunkedTimeRange } from "@/utils/timelineUtil";
import { isReplayCamera } from "@/utils/cameraUtil";
import { getEndOfDayTimestamp } from "@/utils/dateUtil";
import axios from "axios";
import {
@ -1015,12 +1016,14 @@ function MotionReview({
let cameras;
if (!filter || !filter.cameras) {
cameras = Object.values(config.cameras);
cameras = Object.values(config.cameras).filter(
(cam) => !isReplayCamera(cam.name),
);
} else {
const filteredCams = filter.cameras;
cameras = Object.values(config.cameras).filter((cam) =>
filteredCams.includes(cam.name),
cameras = Object.values(config.cameras).filter(
(cam) => filteredCams.includes(cam.name) && !isReplayCamera(cam.name),
);
}

View File

@ -44,6 +44,7 @@ import {
} from "@/components/ui/tooltip";
import type { ProfileState } from "@/types/profile";
import { getProfileColor } from "@/utils/profileColors";
import { isReplayCamera } from "@/utils/cameraUtil";
import { cn } from "@/lib/utils";
import {
Select,
@ -87,7 +88,10 @@ export default function CameraManagementView({
const enabledCameras = useMemo(() => {
if (config) {
return Object.keys(config.cameras)
.filter((camera) => config.cameras[camera].enabled_in_config)
.filter(
(camera) =>
config.cameras[camera].enabled_in_config && !isReplayCamera(camera),
)
.sort((a, b) => {
const orderA = config.cameras[a].ui?.order ?? 0;
const orderB = config.cameras[b].ui?.order ?? 0;
@ -180,7 +184,11 @@ export default function CameraManagementView({
const disabledCameras = useMemo(() => {
if (config) {
return Object.keys(config.cameras)
.filter((camera) => !config.cameras[camera].enabled_in_config)
.filter(
(camera) =>
!config.cameras[camera].enabled_in_config &&
!isReplayCamera(camera),
)
.sort();
}
return [];
@ -188,7 +196,9 @@ export default function CameraManagementView({
const allCameras = useMemo(() => {
if (config) {
return Object.keys(config.cameras).sort();
return Object.keys(config.cameras)
.filter((camera) => !isReplayCamera(camera))
.sort();
}
return [];
}, [config]);

View File

@ -16,6 +16,7 @@ import FrigatePlusCurrentModelSummary from "@/views/settings/components/FrigateP
import { useDocDomain } from "@/hooks/use-doc-domain";
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
import { FrigateConfig } from "@/types/frigateConfig";
import { isReplayCamera } from "@/utils/cameraUtil";
import type { SettingsPageProps } from "@/views/settings/SingleSectionPage";
export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
@ -139,8 +140,9 @@ export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
</tr>
</thead>
<tbody>
{Object.entries(config.cameras).map(
([name, camera]) => (
{Object.entries(config.cameras)
.filter(([name]) => !isReplayCamera(name))
.map(([name, camera]) => (
<tr
key={name}
className="border-b border-secondary"
@ -156,8 +158,7 @@ export default function FrigatePlusSettingsView(_props: SettingsPageProps) {
)}
</td>
</tr>
),
)}
))}
</tbody>
</table>
</div>

View File

@ -19,6 +19,7 @@ import type { JsonObject } from "@/types/configForm";
import type { ProfileState, ProfilesApiResponse } from "@/types/profile";
import { getProfileColor } from "@/utils/profileColors";
import { PROFILE_ELIGIBLE_SECTIONS } from "@/utils/configUtil";
import { isReplayCamera } from "@/utils/cameraUtil";
import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { cn } from "@/lib/utils";
@ -145,7 +146,9 @@ export default function ProfilesView({
if (!config || allProfileNames.length === 0) return {};
const data: Record<string, Record<string, string[]>> = {};
const cameras = Object.keys(config.cameras).sort();
const cameras = Object.keys(config.cameras)
.filter((name) => !isReplayCamera(name))
.sort();
for (const profile of allProfileNames) {
data[profile] = {};

View File

@ -25,6 +25,7 @@ import useSWR from "swr";
import { useTranslation } from "react-i18next";
import { CameraNameLabel } from "@/components/camera/FriendlyNameLabel";
import { resolveCameraName } from "@/hooks/use-camera-friendly-name";
import { isReplayCamera } from "@/utils/cameraUtil";
type CameraMetricsProps = {
lastUpdated: number;
@ -316,7 +317,7 @@ export default function CameraMetrics({
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
{config &&
Object.values(config.cameras).map((camera) => {
if (camera.enabled) {
if (camera.enabled && !isReplayCamera(camera.name)) {
return (
<Fragment key={camera.name}>
{probeCameraName == camera.name && (