diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 975bbddc5..b2656f4a6 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -1342,7 +1342,8 @@ "genai": "GenAI", "face_recognition": "Face Recognition", "lpr": "License Plate Recognition", - "birdseye": "Birdseye" + "birdseye": "Birdseye", + "masksAndZones": "Masks / Zones" }, "detect": { "title": "Detection Settings" @@ -1448,6 +1449,7 @@ "noActiveProfile": "No active profile", "active": "Active", "activated": "Profile '{{profile}}' activated", + "activateFailed": "Failed to set profile", "deactivated": "Profile deactivated", "noProfiles": "No profiles defined. Add a profile from any camera section.", "noOverrides": "No overrides", @@ -1461,7 +1463,8 @@ "deleteProfileConfirm": "Delete profile \"{{profile}}\" from all cameras? This cannot be undone.", "deleteSuccess": "Profile '{{profile}}' deleted", "deleteSection": "Delete Section Overrides", - "deleteSectionConfirm": "Remove {{profile}}'s overrides for {{section}} on {{camera}}?", + "deleteSectionConfirm": "Remove the {{section}} overrides for profile {{profile}} on {{camera}}?", + "deleteSectionSuccess": "Removed {{section}} overrides for {{profile}}", "enableSwitch": "Enable Profiles", "enabledDescription": "Profiles are enabled. Navigate to a camera config section, create a new profile from the dropdown in the header, and save for changes to take effect.", "disabledDescription": "Profiles allow you to define named sets of camera config overrides (e.g., armed, away, night) that can be activated on demand." diff --git a/web/src/components/settings/ProfileSectionDropdown.tsx b/web/src/components/settings/ProfileSectionDropdown.tsx index f8e72b234..fbee6aec7 100644 --- a/web/src/components/settings/ProfileSectionDropdown.tsx +++ b/web/src/components/settings/ProfileSectionDropdown.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { Check, ChevronDown, Plus, Trash2 } from "lucide-react"; import { cn } from "@/lib/utils"; import { getProfileColor } from "@/utils/profileColors"; +import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; import { DropdownMenu, DropdownMenuContent, @@ -52,6 +53,11 @@ export function ProfileSectionDropdown({ onDeleteProfileSection, }: ProfileSectionDropdownProps) { const { t } = useTranslation(["views/settings", "common"]); + const friendlyCameraName = useCameraFriendlyName(cameraName); + const friendlySectionName = t(`configForm.sections.${sectionKey}`, { + ns: "views/settings", + defaultValue: sectionKey, + }); const [addDialogOpen, setAddDialogOpen] = useState(false); const [deleteConfirmProfile, setDeleteConfirmProfile] = useState< string | null @@ -153,21 +159,23 @@ export function ProfileSectionDropdown({ return ( onSelectProfile(profile)} > -
- {isActive && } - - {profile} +
+
+ {isActive && } + + {profile} +
{!hasData && ( - + {t("profiles.noOverrides", { ns: "views/settings" })} )} @@ -260,8 +268,8 @@ export function ProfileSectionDropdown({ {t("profiles.deleteSectionConfirm", { ns: "views/settings", profile: deleteConfirmProfile, - section: sectionKey, - camera: cameraName, + section: friendlySectionName, + camera: friendlyCameraName, })} diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index a06b28edf..da8fffcf6 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -1050,14 +1050,13 @@ export default function Settings() { // Profile state handlers const handleSelectProfile = useCallback( - (camera: string, section: string, profile: string | null) => { - const key = `${camera}::${section}`; + (camera: string, _section: string, profile: string | null) => { setEditingProfile((prev) => { if (profile === null) { - const { [key]: _, ...rest } = prev; + const { [camera]: _, ...rest } = prev; return rest; } - return { ...prev, [key]: profile }; + return { ...prev, [camera]: profile }; }); }, [], @@ -1115,8 +1114,13 @@ export default function Settings() { // Switch back to base config handleSelectProfile(camera, section, null); toast.success( - t("toast.save.success", { - ns: "common", + t("profiles.deleteSectionSuccess", { + ns: "views/settings", + section: t(`configForm.sections.${section}`, { + ns: "views/settings", + defaultValue: section, + }), + profile, }), ); } catch { @@ -1155,8 +1159,7 @@ export default function Settings() { const headerEditingProfile = useMemo(() => { if (!selectedCamera || !currentSectionKey) return null; - const key = `${selectedCamera}::${currentSectionKey}`; - return editingProfile[key] ?? null; + return editingProfile[selectedCamera] ?? null; }, [selectedCamera, currentSectionKey, editingProfile]); const showProfileDropdown = @@ -1230,7 +1233,15 @@ export default function Settings() { }); await mutate("config"); handleSelectProfile(selectedCamera, "masksAndZones", null); - toast.success(t("toast.save.success", { ns: "common" })); + toast.success( + t("profiles.deleteSectionSuccess", { + ns: "views/settings", + section: t("configForm.sections.masksAndZones", { + ns: "views/settings", + }), + profile: profileName, + }), + ); } catch { toast.error(t("toast.save.error.title", { ns: "common" })); } @@ -1639,7 +1650,7 @@ export default function Settings() { )}
-
+
{hasPendingChanges && (
{ if (config && selectedCamera) { diff --git a/web/src/views/settings/ProfilesView.tsx b/web/src/views/settings/ProfilesView.tsx index 83c6221b4..0551d0b94 100644 --- a/web/src/views/settings/ProfilesView.tsx +++ b/web/src/views/settings/ProfilesView.tsx @@ -8,6 +8,7 @@ import type { FrigateConfig } from "@/types/frigateConfig"; import type { ProfileState } from "@/types/profile"; import { getProfileColor } from "@/utils/profileColors"; import { PROFILE_ELIGIBLE_SECTIONS } from "@/utils/configUtil"; +import { resolveCameraName } from "@/hooks/use-camera-friendly-name"; import { cn } from "@/lib/utils"; import Heading from "@/components/ui/heading"; import { Button } from "@/components/ui/button"; @@ -129,10 +130,15 @@ export default function ProfilesView({ : t("profiles.deactivated", { ns: "views/settings" }), { position: "top-center" }, ); - } catch { - toast.error(t("toast.save.error.title", { ns: "common" }), { - position: "top-center", - }); + } catch (err) { + const message = + axios.isAxiosError(err) && err.response?.data?.message + ? String(err.response.data.message) + : undefined; + toast.error( + message || t("profiles.activateFailed", { ns: "views/settings" }), + { position: "top-center" }, + ); } finally { setActivating(false); } @@ -186,10 +192,15 @@ export default function ProfilesView({ }), { position: "top-center" }, ); - } catch { - toast.error(t("toast.save.error.title", { ns: "common" }), { - position: "top-center", - }); + } catch (err) { + const errorMessage = + axios.isAxiosError(err) && err.response?.data?.message + ? String(err.response.data.message) + : undefined; + toast.error( + errorMessage || t("toast.save.error.noMessage", { ns: "common" }), + { position: "top-center" }, + ); } finally { setDeleting(false); setDeleteProfile(null); @@ -371,7 +382,7 @@ export default function ProfilesView({
- {camera} + {resolveCameraName(config, camera)}
{sections.map((section) => ( @@ -382,7 +393,10 @@ export default function ProfilesView({ color.bg, )} > - {section} + {t(`configForm.sections.${section}`, { + ns: "views/settings", + defaultValue: section, + })} ))}
diff --git a/web/src/views/settings/SingleSectionPage.tsx b/web/src/views/settings/SingleSectionPage.tsx index 82c1f3a29..cc166b141 100644 --- a/web/src/views/settings/SingleSectionPage.tsx +++ b/web/src/views/settings/SingleSectionPage.tsx @@ -83,11 +83,8 @@ export function SingleSectionPage({ ? getLocaleDocUrl(resolvedSectionConfig.sectionDocs) : undefined; - const profileKey = selectedCamera - ? `${selectedCamera}::${sectionKey}` - : undefined; - const currentEditingProfile = profileKey - ? (profileState?.editingProfile[profileKey] ?? null) + const currentEditingProfile = selectedCamera + ? (profileState?.editingProfile[selectedCamera] ?? null) : null; const handleSectionStatusChange = useCallback(