Miscellaneous Fixes (0.17 beta) (#21575)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions

* icon improvements

add type to getIconForLabel
provide default icon for audio events

* Add preferred language to review docs

* prevent react Suspense crash during auth redirect

add redirect-check guards to stop rendering lazy routes while navigation is pending (fixes some users seeing React error #426 when auth expires)

* Uppsercase model name

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
This commit is contained in:
Josh Hawkins 2026-01-09 17:23:33 -06:00 committed by GitHub
parent f3543cfee2
commit c08fa15724
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 112 additions and 31 deletions

View File

@ -112,6 +112,17 @@ review:
- animals in the garden - animals in the garden
``` ```
### Preferred Language
By default, review summaries are generated in English. You can configure Frigate to generate summaries in your preferred language by setting the `preferred_language` option:
```yaml
review:
genai:
enabled: true
preferred_language: Spanish
```
## Review Reports ## Review Reports
Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review. Along with individual review item summaries, Generative AI provides the ability to request a report of a given time period. For example, you can get a daily report while on a vacation of any suspicious activity or other concerns that may require review.

View File

@ -15,6 +15,7 @@ import { AuthProvider } from "@/context/auth-context";
import useSWR from "swr"; import useSWR from "swr";
import { FrigateConfig } from "./types/frigateConfig"; import { FrigateConfig } from "./types/frigateConfig";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
import { isRedirectingToLogin } from "@/api/auth-redirect";
const Live = lazy(() => import("@/pages/Live")); const Live = lazy(() => import("@/pages/Live"));
const Events = lazy(() => import("@/pages/Events")); const Events = lazy(() => import("@/pages/Events"));
@ -58,6 +59,16 @@ function DefaultAppView() {
? Object.keys(config.auth.roles) ? Object.keys(config.auth.roles)
: undefined; : undefined;
// Show loading indicator during redirect to prevent React from attempting to render
// lazy components, which would cause error #426 (suspension during synchronous navigation)
if (isRedirectingToLogin()) {
return (
<div className="size-full overflow-hidden">
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
</div>
);
}
return ( return (
<div className="size-full overflow-hidden"> <div className="size-full overflow-hidden">
{isDesktop && <Sidebar />} {isDesktop && <Sidebar />}

View File

@ -28,6 +28,14 @@ export default function ProtectedRoute({
} }
}, [auth.isLoading, auth.isAuthenticated, auth.user]); }, [auth.isLoading, auth.isAuthenticated, auth.user]);
// Show loading indicator during redirect to prevent React from attempting to render
// lazy components, which would cause error #426 (suspension during synchronous navigation)
if (isRedirectingToLogin()) {
return (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
);
}
if (auth.isLoading) { if (auth.isLoading) {
return ( return (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" /> <ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />

View File

@ -181,7 +181,7 @@ export default function ReviewCard({
key={`${object}-${idx}`} key={`${object}-${idx}`}
className="rounded-full bg-muted-foreground p-1" className="rounded-full bg-muted-foreground p-1"
> >
{getIconForLabel(object, "size-3 text-white")} {getIconForLabel(object, "object", "size-3 text-white")}
</div> </div>
))} ))}
{event.data.audio.map((audio, idx) => ( {event.data.audio.map((audio, idx) => (
@ -189,7 +189,7 @@ export default function ReviewCard({
key={`${audio}-${idx}`} key={`${audio}-${idx}`}
className="rounded-full bg-muted-foreground p-1" className="rounded-full bg-muted-foreground p-1"
> >
{getIconForLabel(audio, "size-3 text-white")} {getIconForLabel(audio, "audio", "size-3 text-white")}
</div> </div>
))} ))}
</div> </div>

View File

@ -133,7 +133,11 @@ export default function SearchThumbnail({
className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`} className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs capitalize`}
onClick={() => onClick(searchResult, false, true)} onClick={() => onClick(searchResult, false, true)}
> >
{getIconForLabel(objectLabel, "size-3 text-white")} {getIconForLabel(
objectLabel,
searchResult.data.type,
"size-3 text-white",
)}
{Math.floor( {Math.floor(
(searchResult.data.score ?? (searchResult.data.score ??
searchResult.data.top_score ?? searchResult.data.top_score ??

View File

@ -1296,7 +1296,11 @@ function ObjectDetailsTab({
{t("details.label")} {t("details.label")}
</div> </div>
<div className="flex flex-row items-center gap-2 text-sm smart-capitalize"> <div className="flex flex-row items-center gap-2 text-sm smart-capitalize">
{getIconForLabel(search.label, "size-4 text-primary")} {getIconForLabel(
search.label,
search.data.type,
"size-4 text-primary",
)}
{getTranslatedLabel(search.label, search.data.type)} {getTranslatedLabel(search.label, search.data.type)}
{search.sub_label && ` (${search.sub_label})`} {search.sub_label && ` (${search.sub_label})`}
{isAdmin && search.end_time && ( {isAdmin && search.end_time && (

View File

@ -665,6 +665,7 @@ export function TrackingDetails({
> >
{getIconForLabel( {getIconForLabel(
event.sub_label ? event.label + "-verified" : event.label, event.sub_label ? event.label + "-verified" : event.label,
event.data.type,
"size-4 text-white", "size-4 text-white",
)} )}
</div> </div>

View File

@ -359,7 +359,11 @@ export default function LivePlayer({
]), ]),
] ]
.map((label) => { .map((label) => {
return getIconForLabel(label, "size-3 text-white"); return getIconForLabel(
label,
"object",
"size-3 text-white",
);
}) })
.sort()} .sort()}
</Chip> </Chip>

View File

@ -262,10 +262,18 @@ export default function PreviewThumbnailPlayer({
onClick={() => onClick(review, false, true)} onClick={() => onClick(review, false, true)}
> >
{review.data.objects.sort().map((object) => { {review.data.objects.sort().map((object) => {
return getIconForLabel(object, "size-3 text-white"); return getIconForLabel(
object,
"object",
"size-3 text-white",
);
})} })}
{review.data.audio.map((audio) => { {review.data.audio.map((audio) => {
return getIconForLabel(audio, "size-3 text-white"); return getIconForLabel(
audio,
"audio",
"size-3 text-white",
);
})} })}
</Chip> </Chip>
</> </>

View File

@ -14,6 +14,7 @@ import { FrigateConfig } from "@/types/frigateConfig";
import useSWR from "swr"; import useSWR from "swr";
import ActivityIndicator from "../indicators/activity-indicator"; import ActivityIndicator from "../indicators/activity-indicator";
import { Event } from "@/types/event"; import { Event } from "@/types/event";
import { EventType } from "@/types/search";
import { getIconForLabel } from "@/utils/iconUtil"; import { getIconForLabel } from "@/utils/iconUtil";
import { REVIEW_PADDING, ReviewSegment } from "@/types/review"; import { REVIEW_PADDING, ReviewSegment } from "@/types/review";
import { LuChevronDown, LuCircle, LuChevronRight } from "react-icons/lu"; import { LuChevronDown, LuCircle, LuChevronRight } from "react-icons/lu";
@ -346,22 +347,29 @@ function ReviewGroup({
: null, : null,
); );
const rawIconLabels: string[] = [ const rawIconLabels: Array<{ label: string; type: EventType }> = [
...(fetchedEvents ...(fetchedEvents
? fetchedEvents.map((e) => ? fetchedEvents.map((e) => ({
e.sub_label ? e.label + "-verified" : e.label, label: e.sub_label ? e.label + "-verified" : e.label,
) type: e.data.type,
: (review.data?.objects ?? [])), }))
...(review.data?.audio ?? []), : (review.data?.objects ?? []).map((obj) => ({
label: obj,
type: "object" as EventType,
}))),
...(review.data?.audio ?? []).map((audio) => ({
label: audio,
type: "audio" as EventType,
})),
]; ];
// limit to 5 icons // limit to 5 icons
const seen = new Set<string>(); const seen = new Set<string>();
const iconLabels: string[] = []; const iconLabels: Array<{ label: string; type: EventType }> = [];
for (const lbl of rawIconLabels) { for (const item of rawIconLabels) {
if (!seen.has(lbl)) { if (!seen.has(item.label)) {
seen.add(lbl); seen.add(item.label);
iconLabels.push(lbl); iconLabels.push(item);
if (iconLabels.length >= 5) break; if (iconLabels.length >= 5) break;
} }
} }
@ -418,12 +426,12 @@ function ReviewGroup({
<div className="flex flex-row gap-3"> <div className="flex flex-row gap-3">
<div className="text-sm font-medium">{displayTime}</div> <div className="text-sm font-medium">{displayTime}</div>
<div className="relative flex items-center gap-2 text-white"> <div className="relative flex items-center gap-2 text-white">
{iconLabels.slice(0, 5).map((lbl, idx) => ( {iconLabels.slice(0, 5).map(({ label: lbl, type }, idx) => (
<div <div
key={`${lbl}-${idx}`} key={`${lbl}-${idx}`}
className="rounded-full bg-muted-foreground p-1" className="rounded-full bg-muted-foreground p-1"
> >
{getIconForLabel(lbl, "size-3 text-white")} {getIconForLabel(lbl, type, "size-3 text-white")}
</div> </div>
))} ))}
</div> </div>
@ -516,7 +524,11 @@ function ReviewGroup({
> >
<div className="ml-1.5 flex items-center gap-2 text-sm font-medium"> <div className="ml-1.5 flex items-center gap-2 text-sm font-medium">
<div className="rounded-full bg-muted-foreground p-1"> <div className="rounded-full bg-muted-foreground p-1">
{getIconForLabel(audioLabel, "size-3 text-white")} {getIconForLabel(
audioLabel,
"audio",
"size-3 text-white",
)}
</div> </div>
<span>{getTranslatedLabel(audioLabel, "audio")}</span> <span>{getTranslatedLabel(audioLabel, "audio")}</span>
</div> </div>
@ -618,6 +630,7 @@ function EventList({
> >
{getIconForLabel( {getIconForLabel(
event.sub_label ? event.label + "-verified" : event.label, event.sub_label ? event.label + "-verified" : event.label,
event.data.type,
"size-3 text-white", "size-3 text-white",
)} )}
</div> </div>

View File

@ -1,5 +1,6 @@
import { IconName } from "@/components/icons/IconPicker"; import { IconName } from "@/components/icons/IconPicker";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { EventType } from "@/types/search";
import { BsPersonWalking } from "react-icons/bs"; import { BsPersonWalking } from "react-icons/bs";
import { import {
FaAmazon, FaAmazon,
@ -32,6 +33,7 @@ import {
GiRabbit, GiRabbit,
GiRaccoonHead, GiRaccoonHead,
GiSailboat, GiSailboat,
GiSoundWaves,
GiSquirrel, GiSquirrel,
} from "react-icons/gi"; } from "react-icons/gi";
import { LuBox, LuLassoSelect, LuScanBarcode } from "react-icons/lu"; import { LuBox, LuLassoSelect, LuScanBarcode } from "react-icons/lu";
@ -56,11 +58,15 @@ export function isValidIconName(value: string): value is IconName {
return Object.keys(LuIcons).includes(value as IconName); return Object.keys(LuIcons).includes(value as IconName);
} }
export function getIconForLabel(label: string, className?: string) { export function getIconForLabel(
label: string,
type: EventType = "object",
className?: string,
) {
if (label.endsWith("-verified")) { if (label.endsWith("-verified")) {
return getVerifiedIcon(label, className); return getVerifiedIcon(label, className, type);
} else if (label.endsWith("-plate")) { } else if (label.endsWith("-plate")) {
return getRecognizedPlateIcon(label, className); return getRecognizedPlateIcon(label, className, type);
} }
switch (label) { switch (label) {
@ -152,27 +158,38 @@ export function getIconForLabel(label: string, className?: string) {
case "usps": case "usps":
return <FaUsps key={label} className={className} />; return <FaUsps key={label} className={className} />;
default: default:
if (type === "audio") {
return <GiSoundWaves key={label} className={className} />;
}
return <LuLassoSelect key={label} className={className} />; return <LuLassoSelect key={label} className={className} />;
} }
} }
function getVerifiedIcon(label: string, className?: string) { function getVerifiedIcon(
label: string,
className?: string,
type: EventType = "object",
) {
const simpleLabel = label.substring(0, label.lastIndexOf("-")); const simpleLabel = label.substring(0, label.lastIndexOf("-"));
return ( return (
<div key={label} className="flex items-center"> <div key={label} className="flex items-center">
{getIconForLabel(simpleLabel, className)} {getIconForLabel(simpleLabel, type, className)}
<FaCheckCircle className="absolute size-2 translate-x-[80%] translate-y-3/4" /> <FaCheckCircle className="absolute size-2 translate-x-[80%] translate-y-3/4" />
</div> </div>
); );
} }
function getRecognizedPlateIcon(label: string, className?: string) { function getRecognizedPlateIcon(
label: string,
className?: string,
type: EventType = "object",
) {
const simpleLabel = label.substring(0, label.lastIndexOf("-")); const simpleLabel = label.substring(0, label.lastIndexOf("-"));
return ( return (
<div key={label} className="flex items-center"> <div key={label} className="flex items-center">
{getIconForLabel(simpleLabel, className)} {getIconForLabel(simpleLabel, type, className)}
<LuScanBarcode className="absolute size-2.5 translate-x-[50%] translate-y-3/4" /> <LuScanBarcode className="absolute size-2.5 translate-x-[50%] translate-y-3/4" />
</div> </div>
); );

View File

@ -88,7 +88,7 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
// title // title
useEffect(() => { useEffect(() => {
document.title = `${model.name} - ${t("documentTitle")}`; document.title = `${model.name.toUpperCase()} - ${t("documentTitle")}`;
}, [model.name, t]); }, [model.name, t]);
// model state // model state

View File

@ -406,7 +406,7 @@ function ObjectList({ cameraConfig, objects }: ObjectListProps) {
: getColorForObjectName(obj.label), : getColorForObjectName(obj.label),
}} }}
> >
{getIconForLabel(obj.label, "size-5 text-white")} {getIconForLabel(obj.label, "object", "size-5 text-white")}
</div> </div>
<div className="ml-3 text-lg"> <div className="ml-3 text-lg">
{getTranslatedLabel(obj.label)} {getTranslatedLabel(obj.label)}
@ -494,7 +494,7 @@ function AudioList({ cameraConfig, audioDetections }: AudioListProps) {
<div className="flex flex-row items-center gap-3 pb-1"> <div className="flex flex-row items-center gap-3 pb-1">
<div className="flex flex-1 flex-row items-center justify-start p-3 pl-1"> <div className="flex flex-1 flex-row items-center justify-start p-3 pl-1">
<div className="rounded-lg bg-selected p-2"> <div className="rounded-lg bg-selected p-2">
{getIconForLabel(key, "size-5 text-white")} {getIconForLabel(key, "audio", "size-5 text-white")}
</div> </div>
<div className="ml-3 text-lg">{getTranslatedLabel(key)}</div> <div className="ml-3 text-lg">{getTranslatedLabel(key)}</div>
</div> </div>