mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-10 16:52:47 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48d09efd41 |
@@ -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)**
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user