Skip to content

Commit

Permalink
add frontend support for safe mode
Browse files Browse the repository at this point in the history
  • Loading branch information
roflcoopter committed Mar 15, 2024
1 parent 727fc01 commit f3767a1
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 20 deletions.
26 changes: 26 additions & 0 deletions frontend/src/components/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -175,6 +178,29 @@ export default function AppHeader({ setDrawerOpen }: AppHeaderProps) {
)}
</Stack>
</Container>
{safeMode ? (
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
gap: 2,
backgroundColor: theme.palette.error.main,
}}
>
<Typography
align="center"
style={{
textShadow: "rgba(0, 0, 0, 1) 0px 0px 4px",
margin: "5px",
}}
>
Viseron is running in safe mode. Cameras are not loaded and no
recordings are made. Please check the logs for more information.
</Typography>
</Box>
) : null}
</Header>
);
}
42 changes: 29 additions & 13 deletions frontend/src/context/ViseronContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -31,26 +33,34 @@ export const ViseronProvider: FC<ViseronProviderProps> = ({
const queryClient = useQueryClient();
const toast = useToast();

const [connection, setConnection] = useState<Connection | undefined>(
undefined,
);
const [connected, setConnected] = useState<boolean>(false);
const [contextValue, setContextValue] =
useState<ViseronContextState>(contextDefaultValues);
const { connection } = contextValue;
const onConnectRef = React.useRef<() => void>();
const onDisconnectRef = React.useRef<() => void>();
const onConnectionErrorRef = React.useRef<() => void>();

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) {
Expand Down Expand Up @@ -91,7 +101,10 @@ export const ViseronProvider: FC<ViseronProviderProps> = ({
);
}
connection.disconnect();
setConnection(undefined);
setContextValue((prevContextValue) => ({
...prevContextValue,
connection: undefined,
}));
toast.dismiss(toastIds.websocketConnecting);
toast.dismiss(toastIds.websocketConnectionLost);
}
Expand All @@ -100,12 +113,15 @@ export const ViseronProvider: FC<ViseronProviderProps> = ({
}, [connection, queryClient]);

useEffect(() => {
setConnection(new Connection(toast));
setContextValue((prevContextValue) => ({
...prevContextValue,
connection: new Connection(toast),
}));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<ViseronContext.Provider value={{ connection, connected }}>
<ViseronContext.Provider value={contextValue}>
{children}
</ViseronContext.Provider>
);
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
28 changes: 21 additions & 7 deletions frontend/src/lib/websockets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,18 @@ interface SubscribeEventCommmandInFlight<T> {
unsubscribe: SubscriptionUnsubscribe;
}

export function createSocket(wsURL: string): Promise<WebSocket> {
export type SocketPromise = {
socket: WebSocket;
system_information: types.SystemInformation;
};

export function createSocket(wsURL: string): Promise<SocketPromise> {
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) {
Expand All @@ -75,7 +80,7 @@ export function createSocket(wsURL: string): Promise<WebSocket> {

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);
Expand All @@ -102,7 +107,10 @@ export function createSocket(wsURL: string): Promise<WebSocket> {
// 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) {
Expand Down Expand Up @@ -139,7 +147,10 @@ export function createSocket(wsURL: string): Promise<WebSocket> {
socket.removeEventListener("message", handleMessage);
socket.removeEventListener("close", closeMessage);
socket.removeEventListener("error", closeMessage);
promResolve(socket);
promResolve({
socket,
system_information: message.system_information,
});
break;

default:
Expand All @@ -163,6 +174,8 @@ export function createSocket(wsURL: string): Promise<WebSocket> {
export class Connection {
socket: WebSocket | null = null;

system_information: types.SystemInformation | null = null;

reconnectTimer: NodeJS.Timeout | null = null;

closeRequested = false;
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit f3767a1

Please sign in to comment.