Compare commits

..
1 Commits
Author SHA1 Message Date
Martin WeineltandGitHub 48d09efd41 Merge b0588a02f9 into 48aaafba3c 2026-07-16 22:42:21 +02:00
11 changed files with 39 additions and 102 deletions
-13
View File
@@ -262,19 +262,6 @@ In this example:
- Admin precedence: if the `admin` mapping matches, Frigate resolves the session to `admin` to avoid accidental downgrade when a user belongs to multiple groups (for example both `admin` and `viewer` groups). - Admin precedence: if the `admin` mapping matches, Frigate resolves the session to `admin` to avoid accidental downgrade when a user belongs to multiple groups (for example both `admin` and `viewer` groups).
:::note
If a user isn't getting the role you expect, enable debug logging to see exactly what headers Frigate is receiving from your proxy:
```yaml
logger:
default: info
logs:
frigate.api.auth: debug
```
:::
#### Port Considerations #### Port Considerations
**Authenticated Port (8971)** **Authenticated Port (8971)**
+2
View File
@@ -23,6 +23,7 @@ from frigate.const import (
EXPIRE_AUDIO_ACTIVITY, EXPIRE_AUDIO_ACTIVITY,
INSERT_MANY_RECORDINGS, INSERT_MANY_RECORDINGS,
INSERT_PREVIEW, INSERT_PREVIEW,
NOTIFICATION_TEST,
REQUEST_REGION_GRID, REQUEST_REGION_GRID,
UPDATE_AUDIO_ACTIVITY, UPDATE_AUDIO_ACTIVITY,
UPDATE_AUDIO_TRANSCRIPTION_STATE, UPDATE_AUDIO_TRANSCRIPTION_STATE,
@@ -56,6 +57,7 @@ _WS_BLOCKED_TOPICS = frozenset(
UPDATE_EMBEDDINGS_REINDEX_PROGRESS, UPDATE_EMBEDDINGS_REINDEX_PROGRESS,
UPDATE_BIRDSEYE_LAYOUT, UPDATE_BIRDSEYE_LAYOUT,
UPDATE_AUDIO_TRANSCRIPTION_STATE, UPDATE_AUDIO_TRANSCRIPTION_STATE,
NOTIFICATION_TEST,
} }
) )
@@ -288,10 +288,6 @@ class FaceRealTimeProcessor(RealTimeProcessorApi):
max(0, face_box[0]) : min(frame.shape[1], face_box[2]), max(0, face_box[0]) : min(frame.shape[1], face_box[2]),
] ]
if face_frame.size == 0:
logger.debug(f"Empty face crop for {id}")
return
res = self.recognizer.classify(face_frame) res = self.recognizer.classify(face_frame)
if not res: if not res:
+3 -1
View File
@@ -8,7 +8,9 @@ from frigate.util.builtin import clean_camera_user_pass, escape_special_characte
class TestUserPassCleanup(unittest.TestCase): class TestUserPassCleanup(unittest.TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.rtsp_with_pass = "rtsp://user:password@192.168.0.2:554/live" self.rtsp_with_pass = "rtsp://user:password@192.168.0.2:554/live"
self.rtsp_with_special_pass = "rtsp://user:password`~!@#$%^&*()-_;',.<>:\"\\{\\}\\[\\]@@192.168.0.2:554/live" self.rtsp_with_special_pass = (
"rtsp://user:password`~!@#$%^&*()-_;',.<>:\"\{\}\[\]@@192.168.0.2:554/live"
)
self.rtsp_no_pass = "rtsp://192.168.0.3:554/live" self.rtsp_no_pass = "rtsp://192.168.0.3:554/live"
def test_cleanup(self): def test_cleanup(self):
-14
View File
@@ -115,13 +115,6 @@ class TestCheckWsAuthorization(unittest.TestCase):
) )
) )
def test_viewer_blocked_from_notification_test(self):
self.assertFalse(
_check_ws_authorization(
"notification_test", "viewer", self.DEFAULT_SEPARATOR
)
)
# --- Admin access --- # --- Admin access ---
def test_admin_can_send_restart(self): def test_admin_can_send_restart(self):
@@ -141,13 +134,6 @@ class TestCheckWsAuthorization(unittest.TestCase):
_check_ws_authorization("front_door/ptz", "admin", self.DEFAULT_SEPARATOR) _check_ws_authorization("front_door/ptz", "admin", self.DEFAULT_SEPARATOR)
) )
def test_admin_can_send_notification_test(self):
self.assertTrue(
_check_ws_authorization(
"notification_test", "admin", self.DEFAULT_SEPARATOR
)
)
# --- Comma-separated roles --- # --- Comma-separated roles ---
def test_comma_separated_admin_viewer_grants_admin(self): def test_comma_separated_admin_viewer_grants_admin(self):
+15 -14
View File
@@ -684,21 +684,22 @@ class TrackedObjectProcessor(threading.Thread):
# check for config updates # check for config updates
updated_topics = self.camera_config_subscriber.check_for_updates() updated_topics = self.camera_config_subscriber.check_for_updates()
# a single drain can carry several topics at once, so add and if "enabled" in updated_topics:
# remove are handled independently rather than as exclusive branches for camera in updated_topics["enabled"]:
for camera in updated_topics.get("add", []): if self.camera_states[camera].prev_enabled is None:
self.config.cameras[camera] = ( self.camera_states[camera].prev_enabled = self.config.cameras[
self.camera_config_subscriber.camera_configs[camera] camera
) ].enabled
self.create_camera_state(camera) elif "add" in updated_topics:
for camera in updated_topics["add"]:
if "remove" in updated_topics: self.config.cameras[camera] = (
self.camera_config_subscriber.camera_configs[camera]
)
self.create_camera_state(camera)
elif "remove" in updated_topics:
for camera in updated_topics["remove"]: for camera in updated_topics["remove"]:
camera_state = self.camera_states.get(camera) removed_camera_state = self.camera_states[camera]
if camera_state is None: removed_camera_state.shutdown()
continue
camera_state.shutdown()
self.camera_states.pop(camera) self.camera_states.pop(camera)
self.camera_activity.pop(camera, None) self.camera_activity.pop(camera, None)
self.last_motion_detected.pop(camera, None) self.last_motion_detected.pop(camera, None)
@@ -227,18 +227,16 @@ export function GenAIModelWidget(props: WidgetProps) {
aria-expanded={open} aria-expanded={open}
disabled={disabled || readonly} disabled={disabled || readonly}
className={cn( className={cn(
"min-w-0 justify-between font-normal", "justify-between font-normal",
!currentLabel && "text-muted-foreground", !currentLabel && "text-muted-foreground",
fieldClassName, fieldClassName,
)} )}
> >
<span className="truncate"> {currentLabel ??
{currentLabel ?? t("configForm.genaiModel.placeholder", {
t("configForm.genaiModel.placeholder", { ns: "views/settings",
ns: "views/settings", defaultValue: "Select or enter a model…",
defaultValue: "Select or enter a model…", })}
})}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
@@ -265,14 +263,12 @@ export function GenAIModelWidget(props: WidgetProps) {
value={trimmedSearch} value={trimmedSearch}
onSelect={() => commit(trimmedSearch)} onSelect={() => commit(trimmedSearch)}
> >
<Plus className="mr-2 h-4 w-4 shrink-0" /> <Plus className="mr-2 h-4 w-4" />
<span className="truncate"> {t("configForm.genaiModel.useCustom", {
{t("configForm.genaiModel.useCustom", { ns: "views/settings",
ns: "views/settings", value: trimmedSearch,
value: trimmedSearch, defaultValue: 'Use "{{value}}"',
defaultValue: 'Use "{{value}}"', })}
})}
</span>
</CommandItem> </CommandItem>
</CommandGroup> </CommandGroup>
)} )}
@@ -291,11 +287,11 @@ export function GenAIModelWidget(props: WidgetProps) {
> >
<Check <Check
className={cn( className={cn(
"mr-2 h-4 w-4 shrink-0", "mr-2 h-4 w-4",
value === model ? "opacity-100" : "opacity-0", value === model ? "opacity-100" : "opacity-0",
)} )}
/> />
<span className="truncate">{model}</span> {model}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
@@ -129,7 +129,7 @@ export function GeneralFilterContent({
className="mx-2 w-full cursor-pointer text-primary smart-capitalize" className="mx-2 w-full cursor-pointer text-primary smart-capitalize"
htmlFor={item} htmlFor={item}
> >
{t(`logger.logLevel.${item}`, { ns: "views/settings" })} {item.replaceAll("_", " ")}
</Label> </Label>
<Switch <Switch
key={item} key={item}
+1 -3
View File
@@ -3,7 +3,6 @@ import { LogSeverity } from "@/types/log";
import { ReactNode, useMemo } from "react"; import { ReactNode, useMemo } from "react";
import { isIOS } from "react-device-detect"; import { isIOS } from "react-device-detect";
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { useTranslation } from "react-i18next";
type ChipProps = { type ChipProps = {
className?: string; className?: string;
@@ -51,7 +50,6 @@ type LogChipProps = {
onClickSeverity?: () => void; onClickSeverity?: () => void;
}; };
export function LogChip({ severity, onClickSeverity }: LogChipProps) { export function LogChip({ severity, onClickSeverity }: LogChipProps) {
const { t } = useTranslation(["views/settings"]);
const severityClassName = useMemo(() => { const severityClassName = useMemo(() => {
switch (severity) { switch (severity) {
case "info": case "info":
@@ -75,7 +73,7 @@ export function LogChip({ severity, onClickSeverity }: LogChipProps) {
} }
}} }}
> >
{t(`logger.logLevel.${severity}`, { ns: "views/settings" })} {severity}
</span> </span>
</div> </div>
); );
+1 -4
View File
@@ -60,10 +60,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<CommandPrimitive.List <CommandPrimitive.List
ref={ref} ref={ref}
className={cn( className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
"scrollbar-container max-h-[300px] overflow-y-auto overflow-x-hidden",
className,
)}
{...props} {...props}
/> />
)); ));
@@ -770,34 +770,6 @@ export default function MotionSearchView({
}; };
}, [cancelMotionSearchJobViaBeacon]); }, [cancelMotionSearchJobViaBeacon]);
const handleBack = useCallback(() => {
if (onBack) {
onBack();
} else {
navigate(-1);
}
}, [navigate, onBack]);
// Dismissing the entry dialog (escape / click outside) before a search has
// run leaves nothing behind it, so cancel the flow instead of revealing an
// empty page.
const handleSearchDialogOpenChange = useCallback(
(nextOpen: boolean) => {
if (
!nextOpen &&
!isSearching &&
!hasSearched &&
searchResults.length === 0
) {
handleBack();
return;
}
setIsSearchDialogOpen(nextOpen);
},
[handleBack, hasSearched, isSearching, searchResults.length],
);
const handleNewSearch = useCallback(() => { const handleNewSearch = useCallback(() => {
if (jobId && jobCamera) { if (jobId && jobCamera) {
void cancelMotionSearchJob(jobId, jobCamera); void cancelMotionSearchJob(jobId, jobCamera);
@@ -1266,7 +1238,7 @@ export default function MotionSearchView({
<Toaster closeButton={true} position="top-center" /> <Toaster closeButton={true} position="top-center" />
<MotionSearchDialog <MotionSearchDialog
open={isSearchDialogOpen} open={isSearchDialogOpen}
onOpenChange={handleSearchDialogOpenChange} onOpenChange={setIsSearchDialogOpen}
config={config} config={config}
cameras={cameras} cameras={cameras}
selectedCamera={selectedCamera} selectedCamera={selectedCamera}
@@ -1304,7 +1276,7 @@ export default function MotionSearchView({
className="flex items-center gap-2.5 rounded-lg" className="flex items-center gap-2.5 rounded-lg"
aria-label={t("label.back", { ns: "common" })} aria-label={t("label.back", { ns: "common" })}
size="sm" size="sm"
onClick={handleBack} onClick={() => (onBack ? onBack() : navigate(-1))}
> >
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" /> <IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && ( {isDesktop && (