mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-07-30 13:48:07 +02:00
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:
parent
87d0102624
commit
5f40e6e2b9
@ -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. ***")
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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",
|
||||
|
105
web/src/App.tsx
105
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<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;
|
||||
|
@ -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<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>();
|
||||
@ -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 <ActivityIndicator />;
|
||||
}
|
||||
|
||||
@ -217,9 +221,16 @@ function ConfigEditor() {
|
||||
<div className="absolute bottom-2 left-0 right-0 top-2 md:left-2">
|
||||
<div className="relative h-full 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"
|
||||
|
@ -283,6 +283,9 @@ export type AllGroupsStreamingSettings = {
|
||||
};
|
||||
|
||||
export interface FrigateConfig {
|
||||
version: string;
|
||||
safe_mode: boolean;
|
||||
|
||||
audio: {
|
||||
enabled: boolean;
|
||||
enabled_in_config: boolean | null;
|
||||
|
Loading…
Reference in New Issue
Block a user