From f3767a16665d4b740129d9185be205672879e507 Mon Sep 17 00:00:00 2001 From: Jesper Nilsson Date: Fri, 15 Mar 2024 13:24:05 +0000 Subject: [PATCH] add frontend support for safe mode --- frontend/src/components/header/Header.tsx | 26 ++++++++++++++ frontend/src/context/ViseronContext.tsx | 42 ++++++++++++++++------- frontend/src/lib/types.ts | 32 +++++++++++++++++ frontend/src/lib/websockets.ts | 28 +++++++++++---- 4 files changed, 108 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/header/Header.tsx b/frontend/src/components/header/Header.tsx index 57a46256b..d742f362b 100644 --- a/frontend/src/components/header/Header.tsx +++ b/frontend/src/components/header/Header.tsx @@ -9,6 +9,7 @@ import Container from "@mui/material/Container"; import IconButton from "@mui/material/IconButton"; import Stack from "@mui/material/Stack"; import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; import { alpha, styled, useTheme } from "@mui/material/styles"; import useMediaQuery from "@mui/material/useMediaQuery"; import { useContext, useRef, useState } from "react"; @@ -18,6 +19,7 @@ import ViseronLogo from "svg/viseron-logo.svg?react"; import Breadcrumbs from "components/header/Breadcrumbs"; import { useAuthContext } from "context/AuthContext"; import { ColorModeContext } from "context/ColorModeContext"; +import { ViseronContext } from "context/ViseronContext"; import { useScrollPosition } from "hooks/UseScrollPosition"; import { useToast } from "hooks/UseToast"; import { useAuthLogout } from "lib/api/auth"; @@ -58,6 +60,7 @@ export default function AppHeader({ setDrawerOpen }: AppHeaderProps) { const [showHeader, setShowHeader] = useState(true); const lastTogglePos = useRef(0); const { auth } = useAuthContext(); + const { safeMode } = useContext(ViseronContext); useScrollPosition((prevPos: any, currPos: any) => { // Always show header if we haven't scrolled down more than theme.headerHeight @@ -175,6 +178,29 @@ export default function AppHeader({ setDrawerOpen }: AppHeaderProps) { )} + {safeMode ? ( + + + Viseron is running in safe mode. Cameras are not loaded and no + recordings are made. Please check the logs for more information. + + + ) : null} ); } diff --git a/frontend/src/context/ViseronContext.tsx b/frontend/src/context/ViseronContext.tsx index 8aaf1215c..90fb83cfd 100644 --- a/frontend/src/context/ViseronContext.tsx +++ b/frontend/src/context/ViseronContext.tsx @@ -13,11 +13,13 @@ export type ViseronProviderProps = { export type ViseronContextState = { connection: Connection | undefined; connected: boolean; + safeMode: boolean; }; const contextDefaultValues: ViseronContextState = { connection: undefined, connected: false, + safeMode: false, }; export const ViseronContext = @@ -31,10 +33,9 @@ export const ViseronProvider: FC = ({ const queryClient = useQueryClient(); const toast = useToast(); - const [connection, setConnection] = useState( - undefined, - ); - const [connected, setConnected] = useState(false); + const [contextValue, setContextValue] = + useState(contextDefaultValues); + const { connection } = contextValue; const onConnectRef = React.useRef<() => void>(); const onDisconnectRef = React.useRef<() => void>(); const onConnectionErrorRef = React.useRef<() => void>(); @@ -42,15 +43,24 @@ export const ViseronProvider: FC = ({ useEffect(() => { if (connection) { onConnectRef.current = async () => { - await queryClient.invalidateQueries({ - queryKey: ["camera"], - refetchType: "none", + queryClient.invalidateQueries({ + predicate(query) { + return ( + query.queryKey[0] !== "auth" && query.queryKey[1] !== "enabled" + ); + }, }); - await queryClient.invalidateQueries(["cameras"]); - setConnected(true); + setContextValue((prevContextValue) => ({ + ...prevContextValue, + connected: true, + safeMode: !!connection.system_information?.safe_mode, + })); }; onDisconnectRef.current = async () => { - setConnected(false); + setContextValue((prevContextValue) => ({ + ...prevContextValue, + connected: false, + })); }; onConnectionErrorRef.current = async () => { if (auth.enabled) { @@ -91,7 +101,10 @@ export const ViseronProvider: FC = ({ ); } connection.disconnect(); - setConnection(undefined); + setContextValue((prevContextValue) => ({ + ...prevContextValue, + connection: undefined, + })); toast.dismiss(toastIds.websocketConnecting); toast.dismiss(toastIds.websocketConnectionLost); } @@ -100,12 +113,15 @@ export const ViseronProvider: FC = ({ }, [connection, queryClient]); useEffect(() => { - setConnection(new Connection(toast)); + setContextValue((prevContextValue) => ({ + ...prevContextValue, + connection: new Connection(toast), + })); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( - + {children} ); diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index d52740f78..ca5cbb945 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -1,6 +1,38 @@ import { AxiosError } from "axios"; import { Dayjs } from "dayjs"; +export type SystemInformation = { + safe_mode: boolean; +}; + +type WebSocketAuthOkResponse = { + type: "auth_ok"; + message: string; + system_information: SystemInformation; +}; + +type WebSocketAuthRequiredResponse = { + type: "auth_required"; + message: string; +}; + +type WebSocketAuthNotRequiredResponse = { + type: "auth_not_required"; + message: string; + system_information: SystemInformation; +}; + +type WebSocketAuthInvalidResponse = { + type: "auth_failed"; + message: string; +}; + +export type WebSocketAuthResponse = + | WebSocketAuthOkResponse + | WebSocketAuthRequiredResponse + | WebSocketAuthNotRequiredResponse + | WebSocketAuthInvalidResponse; + type WebSocketPongResponse = { command_id: number; type: "pong"; diff --git a/frontend/src/lib/websockets.ts b/frontend/src/lib/websockets.ts index b1f989275..18ebf4628 100644 --- a/frontend/src/lib/websockets.ts +++ b/frontend/src/lib/websockets.ts @@ -42,13 +42,18 @@ interface SubscribeEventCommmandInFlight { unsubscribe: SubscriptionUnsubscribe; } -export function createSocket(wsURL: string): Promise { +export type SocketPromise = { + socket: WebSocket; + system_information: types.SystemInformation; +}; + +export function createSocket(wsURL: string): Promise { if (DEBUG) { console.debug("[Socket] Initializing", wsURL); } function connect( - promResolve: (socket: WebSocket) => void, + promResolve: ({ socket, system_information }: SocketPromise) => void, promReject: (err: Error) => void, ) { if (DEBUG) { @@ -75,7 +80,7 @@ export function createSocket(wsURL: string): Promise { const handleMessage = async (event: MessageEvent) => { let storedTokens = loadTokens(); - const message = JSON.parse(event.data); + const message: types.WebSocketAuthResponse = JSON.parse(event.data); if (DEBUG) { console.debug("[Socket] Received", message); @@ -102,7 +107,10 @@ export function createSocket(wsURL: string): Promise { // Since we authenticate by partly using cookies, we need to close the // socket and open a new one so the refreshed signature_cookie is sent. socket.close(); - let newSocket: WebSocket; + let newSocket: { + socket: WebSocket; + system_information: types.SystemInformation; + }; try { newSocket = await createSocket(wsURL); } catch (error) { @@ -139,7 +147,10 @@ export function createSocket(wsURL: string): Promise { socket.removeEventListener("message", handleMessage); socket.removeEventListener("close", closeMessage); socket.removeEventListener("error", closeMessage); - promResolve(socket); + promResolve({ + socket, + system_information: message.system_information, + }); break; default: @@ -163,6 +174,8 @@ export function createSocket(wsURL: string): Promise { export class Connection { socket: WebSocket | null = null; + system_information: types.SystemInformation | null = null; + reconnectTimer: NodeJS.Timeout | null = null; closeRequested = false; @@ -206,8 +219,9 @@ export class Connection { // eslint-disable-next-line no-constant-condition while (true) { try { - // eslint-disable-next-line no-await-in-loop - this.socket = await createSocket(wsURL); + ({ socket: this.socket, system_information: this.system_information } = + // eslint-disable-next-line no-await-in-loop + await createSocket(wsURL)); break; } catch (error) { if (error === ERR_INVALID_AUTH) {