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).
:::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
**Authenticated Port (8971)**
+2
View File
@@ -23,6 +23,7 @@ from frigate.const import (
EXPIRE_AUDIO_ACTIVITY,
INSERT_MANY_RECORDINGS,
INSERT_PREVIEW,
NOTIFICATION_TEST,
REQUEST_REGION_GRID,
UPDATE_AUDIO_ACTIVITY,
UPDATE_AUDIO_TRANSCRIPTION_STATE,
@@ -56,6 +57,7 @@ _WS_BLOCKED_TOPICS = frozenset(
UPDATE_EMBEDDINGS_REINDEX_PROGRESS,
UPDATE_BIRDSEYE_LAYOUT,
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]),
]
if face_frame.size == 0:
logger.debug(f"Empty face crop for {id}")
return
res = self.recognizer.classify(face_frame)
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):
def setUp(self) -> None:
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"
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 ---
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)
)
def test_admin_can_send_notification_test(self):
self.assertTrue(
_check_ws_authorization(
"notification_test", "admin", self.DEFAULT_SEPARATOR
)
)
# --- Comma-separated roles ---
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
updated_topics = self.camera_config_subscriber.check_for_updates()
# a single drain can carry several topics at once, so add and
# remove are handled independently rather than as exclusive branches
for camera in updated_topics.get("add", []):
self.config.cameras[camera] = (
self.camera_config_subscriber.camera_configs[camera]
)
self.create_camera_state(camera)
if "remove" in updated_topics:
if "enabled" in updated_topics:
for camera in updated_topics["enabled"]:
if self.camera_states[camera].prev_enabled is None:
self.camera_states[camera].prev_enabled = self.config.cameras[
camera
].enabled
elif "add" in updated_topics:
for camera in updated_topics["add"]:
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"]:
camera_state = self.camera_states.get(camera)
if camera_state is None:
continue
camera_state.shutdown()
removed_camera_state = self.camera_states[camera]
removed_camera_state.shutdown()
self.camera_states.pop(camera)
self.camera_activity.pop(camera, None)
self.last_motion_detected.pop(camera, None)
@@ -227,18 +227,16 @@ export function GenAIModelWidget(props: WidgetProps) {
aria-expanded={open}
disabled={disabled || readonly}
className={cn(
"min-w-0 justify-between font-normal",
"justify-between font-normal",
!currentLabel && "text-muted-foreground",
fieldClassName,
)}
>
<span className="truncate">
{currentLabel ??
t("configForm.genaiModel.placeholder", {
ns: "views/settings",
defaultValue: "Select or enter a model…",
})}
</span>
{currentLabel ??
t("configForm.genaiModel.placeholder", {
ns: "views/settings",
defaultValue: "Select or enter a model…",
})}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
@@ -265,14 +263,12 @@ export function GenAIModelWidget(props: WidgetProps) {
value={trimmedSearch}
onSelect={() => commit(trimmedSearch)}
>
<Plus className="mr-2 h-4 w-4 shrink-0" />
<span className="truncate">
{t("configForm.genaiModel.useCustom", {
ns: "views/settings",
value: trimmedSearch,
defaultValue: 'Use "{{value}}"',
})}
</span>
<Plus className="mr-2 h-4 w-4" />
{t("configForm.genaiModel.useCustom", {
ns: "views/settings",
value: trimmedSearch,
defaultValue: 'Use "{{value}}"',
})}
</CommandItem>
</CommandGroup>
)}
@@ -291,11 +287,11 @@ export function GenAIModelWidget(props: WidgetProps) {
>
<Check
className={cn(
"mr-2 h-4 w-4 shrink-0",
"mr-2 h-4 w-4",
value === model ? "opacity-100" : "opacity-0",
)}
/>
<span className="truncate">{model}</span>
{model}
</CommandItem>
))}
</CommandGroup>
@@ -129,7 +129,7 @@ export function GeneralFilterContent({
className="mx-2 w-full cursor-pointer text-primary smart-capitalize"
htmlFor={item}
>
{t(`logger.logLevel.${item}`, { ns: "views/settings" })}
{item.replaceAll("_", " ")}
</Label>
<Switch
key={item}
+1 -3
View File
@@ -3,7 +3,6 @@ import { LogSeverity } from "@/types/log";
import { ReactNode, useMemo } from "react";
import { isIOS } from "react-device-detect";
import { AnimatePresence, motion } from "framer-motion";
import { useTranslation } from "react-i18next";
type ChipProps = {
className?: string;
@@ -51,7 +50,6 @@ type LogChipProps = {
onClickSeverity?: () => void;
};
export function LogChip({ severity, onClickSeverity }: LogChipProps) {
const { t } = useTranslation(["views/settings"]);
const severityClassName = useMemo(() => {
switch (severity) {
case "info":
@@ -75,7 +73,7 @@ export function LogChip({ severity, onClickSeverity }: LogChipProps) {
}
}}
>
{t(`logger.logLevel.${severity}`, { ns: "views/settings" })}
{severity}
</span>
</div>
);
+1 -4
View File
@@ -60,10 +60,7 @@ const CommandList = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn(
"scrollbar-container max-h-[300px] overflow-y-auto overflow-x-hidden",
className,
)}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
@@ -770,34 +770,6 @@ export default function MotionSearchView({
};
}, [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(() => {
if (jobId && jobCamera) {
void cancelMotionSearchJob(jobId, jobCamera);
@@ -1266,7 +1238,7 @@ export default function MotionSearchView({
<Toaster closeButton={true} position="top-center" />
<MotionSearchDialog
open={isSearchDialogOpen}
onOpenChange={handleSearchDialogOpenChange}
onOpenChange={setIsSearchDialogOpen}
config={config}
cameras={cameras}
selectedCamera={selectedCamera}
@@ -1304,7 +1276,7 @@ export default function MotionSearchView({
className="flex items-center gap-2.5 rounded-lg"
aria-label={t("label.back", { ns: "common" })}
size="sm"
onClick={handleBack}
onClick={() => (onBack ? onBack() : navigate(-1))}
>
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
{isDesktop && (