mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-01 04:16:50 +03:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e101bba140 | ||
|
|
bb642f4f46 | ||
|
|
9105ecf363 | ||
|
|
78e05f1e55 | ||
|
|
4c3e0fdea9 | ||
|
|
fa2452e0c8 |
@@ -262,6 +262,19 @@ 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)**
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ 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,
|
||||||
@@ -57,7 +56,6 @@ _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,6 +288,10 @@ 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:
|
||||||
|
|||||||
@@ -115,6 +115,13 @@ 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):
|
||||||
@@ -134,6 +141,13 @@ 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):
|
||||||
|
|||||||
@@ -684,22 +684,21 @@ 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()
|
||||||
|
|
||||||
if "enabled" in updated_topics:
|
# a single drain can carry several topics at once, so add and
|
||||||
for camera in updated_topics["enabled"]:
|
# remove are handled independently rather than as exclusive branches
|
||||||
if self.camera_states[camera].prev_enabled is None:
|
for camera in updated_topics.get("add", []):
|
||||||
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.config.cameras[camera] = (
|
||||||
self.camera_config_subscriber.camera_configs[camera]
|
self.camera_config_subscriber.camera_configs[camera]
|
||||||
)
|
)
|
||||||
self.create_camera_state(camera)
|
self.create_camera_state(camera)
|
||||||
elif "remove" in updated_topics:
|
|
||||||
|
if "remove" in updated_topics:
|
||||||
for camera in updated_topics["remove"]:
|
for camera in updated_topics["remove"]:
|
||||||
removed_camera_state = self.camera_states[camera]
|
camera_state = self.camera_states.get(camera)
|
||||||
removed_camera_state.shutdown()
|
if camera_state is None:
|
||||||
|
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,16 +227,18 @@ export function GenAIModelWidget(props: WidgetProps) {
|
|||||||
aria-expanded={open}
|
aria-expanded={open}
|
||||||
disabled={disabled || readonly}
|
disabled={disabled || readonly}
|
||||||
className={cn(
|
className={cn(
|
||||||
"justify-between font-normal",
|
"min-w-0 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>
|
||||||
@@ -263,12 +265,14 @@ export function GenAIModelWidget(props: WidgetProps) {
|
|||||||
value={trimmedSearch}
|
value={trimmedSearch}
|
||||||
onSelect={() => commit(trimmedSearch)}
|
onSelect={() => commit(trimmedSearch)}
|
||||||
>
|
>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4 shrink-0" />
|
||||||
|
<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>
|
||||||
)}
|
)}
|
||||||
@@ -287,11 +291,11 @@ export function GenAIModelWidget(props: WidgetProps) {
|
|||||||
>
|
>
|
||||||
<Check
|
<Check
|
||||||
className={cn(
|
className={cn(
|
||||||
"mr-2 h-4 w-4",
|
"mr-2 h-4 w-4 shrink-0",
|
||||||
value === model ? "opacity-100" : "opacity-0",
|
value === model ? "opacity-100" : "opacity-0",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{model}
|
<span className="truncate">{model}</span>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
|||||||
@@ -770,6 +770,34 @@ 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);
|
||||||
@@ -1238,7 +1266,7 @@ export default function MotionSearchView({
|
|||||||
<Toaster closeButton={true} position="top-center" />
|
<Toaster closeButton={true} position="top-center" />
|
||||||
<MotionSearchDialog
|
<MotionSearchDialog
|
||||||
open={isSearchDialogOpen}
|
open={isSearchDialogOpen}
|
||||||
onOpenChange={setIsSearchDialogOpen}
|
onOpenChange={handleSearchDialogOpenChange}
|
||||||
config={config}
|
config={config}
|
||||||
cameras={cameras}
|
cameras={cameras}
|
||||||
selectedCamera={selectedCamera}
|
selectedCamera={selectedCamera}
|
||||||
@@ -1276,7 +1304,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={() => (onBack ? onBack() : navigate(-1))}
|
onClick={handleBack}
|
||||||
>
|
>
|
||||||
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
<IoMdArrowRoundBack className="size-5 text-secondary-foreground" />
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
|
|||||||
Reference in New Issue
Block a user