Files
frigate/web/src/App.tsx
T
Josh HawkinsandGitHub 79177db969 Update deps (#24623)
* bump backend and frontend dependencies

Bumps every outdated dependency that passed testing, with the code changes each one needs.

- peewee 4.5 and peewee_migrate 2.3: use SqliteDatabase in place of the removed SqliteExtDatabase, and fix types for peewee's bundled stubs
- mypy 2.4.0: remove unused type ignores and add the annotations mypy 2 needs
- pandas 3.0: convert the motion activity index with as_unit("s"), because pandas 3 infers second resolution for whole-second timestamps and the old // 10**9 returned 1
- transformers 5.19: load Jina tokenizers from the saved directory with trust_remote_code off and use CLIPImageProcessor, since v5 removed CLIPFeatureExtractor and remote code pulled in torch
- tensorflow-cpu 2.21: pin protobuf to 7.36 in the TensorRT amd64 image, because the old 3.20.3 pin was installed over TensorFlow and broke its import
- pydantic 2.13 and google-genai 2.29: regenerate the API spec
- ruff 0.16: set an explicit lint select, since 0.16 widened the default rules
- react-router-dom 7: set useTransitions={false} on BrowserRouter to keep v6 update timing, which useSearchEffect relies on
- typescript 6: drop baseUrl and add node types in tsconfig
- radix: update to the latest release and remove the slot patch, which upstream now includes
- apexcharts 7.8: guard the tooltip formatter against null values
- cryptography 50, joserfc 1.7, pywebpush 2.5, openai 3.26, aiohttp, uvicorn, onvif-zeep-async, scipy, framer-motion 14, react-day-picker 10 and other minor and patch bumps

* silence httpx2 request logging

* redownload an incomplete jina tokenizer

An interrupted tokenizer download left the Hugging Face cache directory without the saved tokenizer files, and since the download is skipped whenever that directory exists, every restart failed to load the tokenizer. The Jina constructors now remove a tokenizer directory that has no tokenizer_config.json so the download runs again.
2026-10-10 15:01:33 -05:00

175 lines
6.6 KiB
TypeScript

import Providers from "@/context/providers";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Wrapper from "@/components/Wrapper";
import Sidebar from "@/components/navigation/Sidebar";
import CommandMenu from "@/components/menu/CommandMenu";
import { isDesktop, isMobile } from "react-device-detect";
import Statusbar from "./components/Statusbar";
import Bottombar from "./components/navigation/Bottombar";
import { lazy, useContext, useEffect, useState } from "react";
import { Redirect } from "./components/navigation/Redirect";
import { cn } from "./lib/utils";
import { isPWA } from "./utils/isPWA";
import ProtectedRoute from "@/components/auth/ProtectedRoute";
import useSWR from "swr";
import { FrigateConfig } from "./types/frigateConfig";
import ActivityIndicator from "@/components/indicators/activity-indicator";
import { isRedirectingToLogin } from "@/api/auth-redirect";
import { AuthContext } from "@/context/auth-context";
import { useIsAdmin } from "@/hooks/use-is-admin";
import { isSetupDismissed } from "@/utils/setupWizard";
import { ChromeErrorBoundary, LazyPage } from "@/components/ErrorBoundaries";
const Live = lazy(() => import("@/pages/Live"));
const Events = lazy(() => import("@/pages/Events"));
const Explore = lazy(() => import("@/pages/Explore"));
const Exports = lazy(() => import("@/pages/Exports"));
const ConfigEditor = lazy(() => import("@/pages/ConfigEditor"));
const System = lazy(() => import("@/pages/System"));
const Settings = lazy(() => import("@/pages/Settings"));
const UIPlayground = lazy(() => import("@/pages/UIPlayground"));
const FaceLibrary = lazy(() => import("@/pages/FaceLibrary"));
const Classification = lazy(() => import("@/pages/ClassificationModel"));
const Chat = lazy(() => import("@/pages/Chat"));
const Logs = lazy(() => import("@/pages/Logs"));
const AccessDenied = lazy(() => import("@/pages/AccessDenied"));
const Replay = lazy(() => import("@/pages/Replay"));
const SetupWizard = lazy(() => import("@/pages/SetupWizard"));
function App() {
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
return (
<Providers>
<BrowserRouter basename={window.baseUrl} useTransitions={false}>
<Wrapper>
{config?.safe_mode ? <SafeAppView /> : <DefaultAppView />}
</Wrapper>
</BrowserRouter>
</Providers>
);
}
function DefaultAppView() {
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
// decided once per load: adding the first camera part way through the
// wizard must not pull the wizard out from under the user
const [showWizard, setShowWizard] = useState<boolean>();
const { auth } = useContext(AuthContext);
const isAdmin = useIsAdmin();
useEffect(() => {
// every step writes through admin only endpoints, and the role isn't
// known until the profile resolves
if (config && !auth.isLoading && showWizard === undefined) {
setShowWizard(
isAdmin &&
Object.keys(config.cameras ?? {}).length === 0 &&
!isSetupDismissed(),
);
}
}, [config, auth.isLoading, isAdmin, showWizard]);
// Compute required roles for main routes, ensuring we have config first
// to prevent race condition where custom roles are temporarily unavailable
const mainRouteRoles = config?.auth?.roles
? Object.keys(config.auth.roles)
: 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>
);
}
// Show setup wizard for first-time users
if (showWizard) {
return (
<div className="size-full overflow-hidden">
<LazyPage
fallback={
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
}
>
<SetupWizard />
</LazyPage>
</div>
);
}
return (
<div className="size-full overflow-hidden">
<ChromeErrorBoundary>{isDesktop && <Sidebar />}</ChromeErrorBoundary>
<ChromeErrorBoundary>{isDesktop && <Statusbar />}</ChromeErrorBoundary>
<ChromeErrorBoundary>{isDesktop && <CommandMenu />}</ChromeErrorBoundary>
<ChromeErrorBoundary>{isMobile && <Bottombar />}</ChromeErrorBoundary>
<div
id="pageRoot"
className={cn(
"absolute right-0 top-0 overflow-hidden",
isMobile
? isPWA
? "bottom-[calc(3rem+env(safe-area-inset-bottom))] left-0 pt-[env(safe-area-inset-top)] md:bottom-[calc(4rem+env(safe-area-inset-bottom))] landscape:pl-[env(safe-area-inset-left)] landscape:pr-[env(safe-area-inset-right)]"
: "bottom-12 left-0 md:bottom-16"
: "bottom-8 left-[52px]",
)}
>
<LazyPage
fallback={
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
}
>
<Routes>
<Route element={<ProtectedRoute requiredRoles={mainRouteRoles} />}>
<Route index element={<Live />} />
<Route path="/review" element={<Events />} />
<Route path="/explore" element={<Explore />} />
<Route path="/export" element={<Exports />} />
<Route path="/settings" element={<Settings />} />
</Route>
<Route element={<ProtectedRoute requiredRoles={["admin"]} />}>
<Route path="/system" element={<System />} />
<Route path="/config" element={<ConfigEditor />} />
<Route path="/logs" element={<Logs />} />
<Route path="/faces" element={<FaceLibrary />} />
<Route path="/classification" element={<Classification />} />
<Route path="/chat" element={<Chat />} />
<Route path="/playground" element={<UIPlayground />} />{" "}
<Route path="/replay" element={<Replay />} />{" "}
</Route>
<Route path="/unauthorized" element={<AccessDenied />} />
<Route path="*" element={<Redirect to="/" />} />
</Routes>
</LazyPage>
</div>
</div>
);
}
function SafeAppView() {
return (
<div className="size-full overflow-hidden">
<div
id="pageRoot"
className={cn("absolute bottom-0 left-0 right-0 top-0 overflow-hidden")}
>
<LazyPage>
<ConfigEditor />
</LazyPage>
</div>
</div>
);
}
export default App;