Add basic config editor when Frigate can't startup (#18383)

* Start Frigate in safe mode when config does not validate

* Add safe mode page that is just the config editor

* Adjust Frigate config editor when in safe mode

* Cleanup

* Improve log message
This commit is contained in:
Nicolas Mowen
2025-05-24 10:47:15 -06:00
committed by Blake Blackshear
parent 723553edb7
commit cf1d50be30
6 changed files with 120 additions and 58 deletions

View File

@@ -1,6 +1,8 @@
{
"documentTitle": "Config Editor - Frigate",
"configEditor": "Config Editor",
"safeConfigEditor": "Config Editor (Safe Mode)",
"safeModeDescription": "Frigate is in safe mode due to a config validation error.",
"copyConfig": "Copy Config",
"saveAndRestart": "Save & Restart",
"saveOnly": "Save Only",

View File

@@ -12,6 +12,8 @@ import { cn } from "./lib/utils";
import { isPWA } from "./utils/isPWA";
import ProtectedRoute from "@/components/auth/ProtectedRoute";
import { AuthProvider } from "@/context/auth-context";
import useSWR from "swr";
import { FrigateConfig } from "./types/frigateConfig";
const Live = lazy(() => import("@/pages/Live"));
const Events = lazy(() => import("@/pages/Events"));
@@ -26,52 +28,16 @@ const Logs = lazy(() => import("@/pages/Logs"));
const AccessDenied = lazy(() => import("@/pages/AccessDenied"));
function App() {
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
return (
<Providers>
<AuthProvider>
<BrowserRouter basename={window.baseUrl}>
<Wrapper>
<div className="size-full overflow-hidden">
{isDesktop && <Sidebar />}
{isDesktop && <Statusbar />}
{isMobile && <Bottombar />}
<div
id="pageRoot"
className={cn(
"absolute right-0 top-0 overflow-hidden",
isMobile
? `bottom-${isPWA ? 16 : 12} left-0 md:bottom-16 landscape:bottom-14 landscape:md:bottom-16`
: "bottom-8 left-[52px]",
)}
>
<Suspense>
<Routes>
<Route
element={
<ProtectedRoute requiredRoles={["viewer", "admin"]} />
}
>
<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="/playground" element={<UIPlayground />} />
</Route>
<Route path="/unauthorized" element={<AccessDenied />} />
<Route path="*" element={<Redirect to="/" />} />
</Routes>
</Suspense>
</div>
</div>
{config?.safe_mode ? <SafeAppView /> : <DefaultAppView />}
</Wrapper>
</BrowserRouter>
</AuthProvider>
@@ -79,4 +45,61 @@ function App() {
);
}
function DefaultAppView() {
return (
<div className="size-full overflow-hidden">
{isDesktop && <Sidebar />}
{isDesktop && <Statusbar />}
{isMobile && <Bottombar />}
<div
id="pageRoot"
className={cn(
"absolute right-0 top-0 overflow-hidden",
isMobile
? `bottom-${isPWA ? 16 : 12} left-0 md:bottom-16 landscape:bottom-14 landscape:md:bottom-16`
: "bottom-8 left-[52px]",
)}
>
<Suspense>
<Routes>
<Route
element={<ProtectedRoute requiredRoles={["viewer", "admin"]} />}
>
<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="/playground" element={<UIPlayground />} />
</Route>
<Route path="/unauthorized" element={<AccessDenied />} />
<Route path="*" element={<Redirect to="/" />} />
</Routes>
</Suspense>
</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")}
>
<Suspense>
<ConfigEditor />
</Suspense>
</div>
</div>
);
}
export default App;

View File

@@ -16,7 +16,11 @@ import { MdOutlineRestartAlt } from "react-icons/md";
import RestartDialog from "@/components/overlay/dialog/RestartDialog";
import { useTranslation } from "react-i18next";
import { useRestart } from "@/api/ws";
<<<<<<< HEAD
import { useResizeObserver } from "@/hooks/resize-observer";
=======
import { FrigateConfig } from "@/types/frigateConfig";
>>>>>>> 5f40e6e2 (Add basic config editor when Frigate can't startup (#18383))
type SaveOptions = "saveonly" | "restart";
@@ -33,7 +37,10 @@ function ConfigEditor() {
document.title = t("documentTitle");
}, [t]);
const { data: config } = useSWR<string>("config/raw");
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
const { data: rawConfig } = useSWR<string>("config/raw");
const { theme, systemTheme } = useTheme();
const [error, setError] = useState<string | undefined>();
@@ -103,7 +110,7 @@ function ConfigEditor() {
}, [onHandleSaveConfig]);
useEffect(() => {
if (!config) {
if (!rawConfig) {
return;
}
@@ -130,9 +137,9 @@ function ConfigEditor() {
}
if (!modelRef.current) {
modelRef.current = monaco.editor.createModel(config, "yaml", modelUri);
modelRef.current = monaco.editor.createModel(rawConfig, "yaml", modelUri);
} else {
modelRef.current.setValue(config);
modelRef.current.setValue(rawConfig);
}
const container = configRef.current;
@@ -165,32 +172,32 @@ function ConfigEditor() {
}
schemaConfiguredRef.current = false;
};
}, [config, apiHost, systemTheme, theme, onHandleSaveConfig]);
}, [rawConfig, apiHost, systemTheme, theme, onHandleSaveConfig]);
// monitoring state
const [hasChanges, setHasChanges] = useState(false);
useEffect(() => {
if (!config || !modelRef.current) {
if (!rawConfig || !modelRef.current) {
return;
}
modelRef.current.onDidChangeContent(() => {
if (modelRef.current?.getValue() != config) {
if (modelRef.current?.getValue() != rawConfig) {
setHasChanges(true);
} else {
setHasChanges(false);
}
});
}, [config]);
}, [rawConfig]);
useEffect(() => {
if (config && modelRef.current) {
modelRef.current.setValue(config);
if (rawConfig && modelRef.current) {
modelRef.current.setValue(rawConfig);
setHasChanges(false);
}
}, [config]);
}, [rawConfig]);
useEffect(() => {
let listener: ((e: BeforeUnloadEvent) => void) | undefined;
@@ -225,7 +232,7 @@ function ConfigEditor() {
}
}, [error, width, height]);
if (!config) {
if (!rawConfig) {
return <ActivityIndicator />;
}
@@ -233,9 +240,16 @@ function ConfigEditor() {
<div className="absolute bottom-2 left-0 right-0 top-2 md:left-2">
<div className="relative flex h-full flex-col overflow-hidden">
<div className="mr-1 flex items-center justify-between">
<Heading as="h2" className="mb-0 ml-1 md:ml-0">
{t("configEditor")}
</Heading>
<div>
<Heading as="h2" className="mb-0 ml-1 md:ml-0">
{t(config?.safe_mode ? "safeConfigEditor" : "configEditor")}
</Heading>
{config?.safe_mode && (
<div className="text-sm text-secondary-foreground">
{t("safeModeDescription")}
</div>
)}
</div>
<div className="flex flex-row gap-1">
<Button
size="sm"

View File

@@ -283,6 +283,9 @@ export type AllGroupsStreamingSettings = {
};
export interface FrigateConfig {
version: string;
safe_mode: boolean;
audio: {
enabled: boolean;
enabled_in_config: boolean | null;