Compare commits

..

2 Commits

Author SHA1 Message Date
Nicolas Mowen
eefa532ecc Improve model titles 2025-12-04 12:25:51 -07:00
Nicolas Mowen
c136e5e8bd
Miscellaneous fixes (#21141)
Some checks failed
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
* Remove source_type from API

* Don't require state classification models to select all classes

* Specifically validate provided end_time for manual events

* Remove yolov9 specification for warning

* Remove warning for coral

* clarify zone name tip

* clarify replace rules in lpr docs

* remove periods

* Add explanation for review report

* adjust HLS gap controller params

defaults to false, should help to recover from hangs and stalling in tracking details videos on chrome

* only redirect to login page once on 401

attempt to fix ios pwa safari redirect storm

* Use contextual information from other cameras to inform report summary

* Formatting and prompt improvements for review summary report

* More improvements to prompt

* Remove examples

* Don't show admin action buttons on export card

* fix redirect race condition

Coordinate 401 redirect logic between ApiProvider and ProtectedRoute using a shared flag to prevent multiple simultaneous redirects that caused UI flashing. Ensure both auth error paths check and set the redirect flag before navigating to login, eliminating race conditions where both mechanisms could trigger at once

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2025-12-04 12:19:07 -06:00
5 changed files with 44 additions and 9 deletions

View File

@ -1,5 +1,5 @@
{ {
"documentTitle": "Classification Models", "documentTitle": "Classification Models - Frigate",
"details": { "details": {
"scoreInfo": "Score represents the average classification confidence across all detections of this object." "scoreInfo": "Score represents the average classification confidence across all detections of this object."
}, },

View File

@ -0,0 +1,12 @@
// Module-level flag to prevent multiple simultaneous redirects
// (eg, when multiple SWR queries fail with 401 at once, or when
// both ApiProvider and ProtectedRoute try to redirect)
let _isRedirectingToLogin = false;
export function isRedirectingToLogin(): boolean {
return _isRedirectingToLogin;
}
export function setRedirectingToLogin(value: boolean): void {
_isRedirectingToLogin = value;
}

View File

@ -3,13 +3,10 @@ import { SWRConfig } from "swr";
import { WsProvider } from "./ws"; import { WsProvider } from "./ws";
import axios from "axios"; import axios from "axios";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { isRedirectingToLogin, setRedirectingToLogin } from "./auth-redirect";
axios.defaults.baseURL = `${baseUrl}api/`; axios.defaults.baseURL = `${baseUrl}api/`;
// Module-level flag to prevent multiple simultaneous redirects
// (eg, when multiple SWR queries fail with 401 at once)
let isRedirectingToLogin = false;
type ApiProviderType = { type ApiProviderType = {
children?: ReactNode; children?: ReactNode;
options?: Record<string, unknown>; options?: Record<string, unknown>;
@ -35,8 +32,8 @@ export function ApiProvider({ children, options }: ApiProviderType) {
) { ) {
// redirect to the login page if not already there // redirect to the login page if not already there
const loginPage = error.response.headers.get("location") ?? "login"; const loginPage = error.response.headers.get("location") ?? "login";
if (window.location.href !== loginPage && !isRedirectingToLogin) { if (window.location.href !== loginPage && !isRedirectingToLogin()) {
isRedirectingToLogin = true; setRedirectingToLogin(true);
window.location.href = loginPage; window.location.href = loginPage;
} }
} }

View File

@ -1,7 +1,11 @@
import { useContext } from "react"; import { useContext, useEffect } from "react";
import { Navigate, Outlet } from "react-router-dom"; import { Navigate, Outlet } from "react-router-dom";
import { AuthContext } from "@/context/auth-context"; import { AuthContext } from "@/context/auth-context";
import ActivityIndicator from "../indicators/activity-indicator"; import ActivityIndicator from "../indicators/activity-indicator";
import {
isRedirectingToLogin,
setRedirectingToLogin,
} from "@/api/auth-redirect";
export default function ProtectedRoute({ export default function ProtectedRoute({
requiredRoles, requiredRoles,
@ -10,6 +14,20 @@ export default function ProtectedRoute({
}) { }) {
const { auth } = useContext(AuthContext); const { auth } = useContext(AuthContext);
// Redirect to login page when not authenticated
// don't use <Navigate> because we need a full page load to reset state
useEffect(() => {
if (
!auth.isLoading &&
auth.isAuthenticated &&
!auth.user &&
!isRedirectingToLogin()
) {
setRedirectingToLogin(true);
window.location.href = "/login";
}
}, [auth.isLoading, auth.isAuthenticated, auth.user]);
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" />
@ -23,7 +41,9 @@ export default function ProtectedRoute({
// Authenticated mode (8971): require login // Authenticated mode (8971): require login
if (!auth.user) { if (!auth.user) {
return <Navigate to="/login" replace />; return (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
);
} }
// If role is null (shouldnt happen if isAuthenticated, but type safety), fallback // If role is null (shouldnt happen if isAuthenticated, but type safety), fallback

View File

@ -84,6 +84,12 @@ export default function ModelTrainingView({ model }: ModelTrainingViewProps) {
const [page, setPage] = useState<string>("train"); const [page, setPage] = useState<string>("train");
const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100); const [pageToggle, setPageToggle] = useOptimisticState(page, setPage, 100);
// title
useEffect(() => {
document.title = `${model.name} - ${t("documentTitle")}`;
}, [model.name, t]);
// model state // model state
const [wasTraining, setWasTraining] = useState(false); const [wasTraining, setWasTraining] = useState(false);