From 5f40e6e2b947244ec84923a3153b5e737972f242 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 24 May 2025 10:47:15 -0600 Subject: [PATCH] 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 --- frigate/__main__.py | 9 +- frigate/config/config.py | 15 ++- web/public/locales/en/views/configEditor.json | 2 + web/src/App.tsx | 105 +++++++++++------- web/src/pages/ConfigEditor.tsx | 41 ++++--- web/src/types/frigateConfig.ts | 3 + 6 files changed, 117 insertions(+), 58 deletions(-) diff --git a/frigate/__main__.py b/frigate/__main__.py index 4143f7ae6..4c732be80 100644 --- a/frigate/__main__.py +++ b/frigate/__main__.py @@ -93,7 +93,14 @@ def main() -> None: print("*************************************************************") print("*** End Config Validation Errors ***") print("*************************************************************") - sys.exit(1) + + # attempt to start Frigate in recovery mode + try: + config = FrigateConfig.load(install=True, safe_load=True) + print("Starting Frigate in safe mode.") + except ValidationError: + print("Unable to start Frigate in safe mode.") + sys.exit(1) if args.validate_config: print("*************************************************************") print("*** Your config file is valid. ***") diff --git a/frigate/config/config.py b/frigate/config/config.py index 6ec048acd..58427f5d5 100644 --- a/frigate/config/config.py +++ b/frigate/config/config.py @@ -334,6 +334,9 @@ def verify_lpr_and_face( class FrigateConfig(FrigateBaseModel): version: Optional[str] = Field(default=None, title="Current config version.") + safe_mode: bool = Field( + default=False, title="If Frigate should be started in safe mode." + ) # Fields that install global state should be defined first, so that their validators run first. environment_vars: EnvVars = Field( @@ -716,6 +719,7 @@ class FrigateConfig(FrigateBaseModel): @classmethod def load(cls, **kwargs): + """Loads the Frigate config file, runs migrations, and creates the config object.""" config_path = find_config_file() # No configuration file found, create one. @@ -743,7 +747,7 @@ class FrigateConfig(FrigateBaseModel): return FrigateConfig.parse(f, **kwargs) @classmethod - def parse(cls, config, *, is_json=None, **context): + def parse(cls, config, *, is_json=None, safe_load=False, **context): # If config is a file, read its contents. if hasattr(config, "read"): fname = getattr(config, "name", None) @@ -767,6 +771,15 @@ class FrigateConfig(FrigateBaseModel): else: config = yaml.load(config) + # load minimal Frigate config after the full config did not validate + if safe_load: + safe_config = {"safe_mode": True, "cameras": {}, "mqtt": {"enabled": False}} + + # copy over auth and proxy config in case auth needs to be enforced + safe_config["auth"] = config.get("auth", {}) + safe_config["proxy"] = config.get("proxy", {}) + return cls.parse_object(safe_config, **context) + # Validate and return the config dict. return cls.parse_object(config, **context) diff --git a/web/public/locales/en/views/configEditor.json b/web/public/locales/en/views/configEditor.json index ef3035f38..614143c16 100644 --- a/web/public/locales/en/views/configEditor.json +++ b/web/public/locales/en/views/configEditor.json @@ -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", diff --git a/web/src/App.tsx b/web/src/App.tsx index a0062549f..d3edbc3a2 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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("config", { + revalidateOnFocus: false, + }); + return ( -
- {isDesktop && } - {isDesktop && } - {isMobile && } -
- - - - } - > - } /> - } /> - } /> - } /> - } /> - - } - > - } /> - } /> - } /> - } /> - } /> - - } /> - } /> - - -
-
+ {config?.safe_mode ? : }
@@ -79,4 +45,61 @@ function App() { ); } +function DefaultAppView() { + return ( +
+ {isDesktop && } + {isDesktop && } + {isMobile && } +
+ + + } + > + } /> + } /> + } /> + } /> + } /> + + }> + } /> + } /> + } /> + } /> + } /> + + } /> + } /> + + +
+
+ ); +} + +function SafeAppView() { + return ( +
+
+ + + +
+
+ ); +} + export default App; diff --git a/web/src/pages/ConfigEditor.tsx b/web/src/pages/ConfigEditor.tsx index 01d76303d..8d3682b76 100644 --- a/web/src/pages/ConfigEditor.tsx +++ b/web/src/pages/ConfigEditor.tsx @@ -16,6 +16,7 @@ import { MdOutlineRestartAlt } from "react-icons/md"; import RestartDialog from "@/components/overlay/dialog/RestartDialog"; import { useTranslation } from "react-i18next"; import { useRestart } from "@/api/ws"; +import { FrigateConfig } from "@/types/frigateConfig"; type SaveOptions = "saveonly" | "restart"; @@ -32,7 +33,10 @@ function ConfigEditor() { document.title = t("documentTitle"); }, [t]); - const { data: config } = useSWR("config/raw"); + const { data: config } = useSWR("config", { + revalidateOnFocus: false, + }); + const { data: rawConfig } = useSWR("config/raw"); const { theme, systemTheme } = useTheme(); const [error, setError] = useState(); @@ -102,7 +106,7 @@ function ConfigEditor() { }, [onHandleSaveConfig]); useEffect(() => { - if (!config) { + if (!rawConfig) { return; } @@ -129,9 +133,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; @@ -164,32 +168,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; @@ -209,7 +213,7 @@ function ConfigEditor() { }; }, [hasChanges, t]); - if (!config) { + if (!rawConfig) { return ; } @@ -217,9 +221,16 @@ function ConfigEditor() {
- - {t("configEditor")} - +
+ + {t(config?.safe_mode ? "safeConfigEditor" : "configEditor")} + + {config?.safe_mode && ( +
+ {t("safeModeDescription")} +
+ )} +