From 324953d3a5dde82f38961981b048479a58313599 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:57:42 -0500 Subject: [PATCH] UI tweaks (#22405) * add shm frame lifetime calculation and update UI for shared memory metrics * consistent sizing on activity indicator in save buttons * fix offline overlay overflowing on mobile when in grid mode --- web/public/locales/en/views/system.json | 6 +- .../components/filter/CameraGroupSelector.tsx | 2 +- .../components/overlay/CreateRoleDialog.tsx | 2 +- .../overlay/CreateTriggerDialog.tsx | 2 +- .../components/overlay/CreateUserDialog.tsx | 2 +- .../overlay/EditRoleCamerasDialog.tsx | 2 +- .../components/overlay/SetPasswordDialog.tsx | 2 +- .../overlay/detail/AnnotationSettingsPane.tsx | 2 +- web/src/components/player/LivePlayer.tsx | 59 +++++--- .../settings/CameraStreamingDialog.tsx | 2 +- .../settings/MotionMaskEditPane.tsx | 2 +- .../settings/ObjectMaskEditPane.tsx | 2 +- web/src/components/settings/ZoneEditPane.tsx | 2 +- web/src/types/stats.ts | 1 + .../settings/EnrichmentsSettingsView.tsx | 2 +- web/src/views/settings/MotionTunerView.tsx | 2 +- web/src/views/system/StorageMetrics.tsx | 126 +++++++++++++----- 17 files changed, 147 insertions(+), 71 deletions(-) diff --git a/web/public/locales/en/views/system.json b/web/public/locales/en/views/system.json index faaff31c9..460a1d337 100644 --- a/web/public/locales/en/views/system.json +++ b/web/public/locales/en/views/system.json @@ -135,7 +135,11 @@ }, "shm": { "title": "SHM (shared memory) allocation", - "warning": "The current SHM size of {{total}}MB is too small. Increase it to at least {{min_shm}}MB." + "warning": "The current SHM size of {{total}}MB is too small. Increase it to at least {{min_shm}}MB.", + "frameLifetime": { + "title": "Frame lifetime", + "description": "Each camera has {{frames}} frame slots in shared memory. At the fastest camera's frame rate, each frame is available for approximately {{lifetime}}s before being overwritten." + } }, "cameraStorage": { "title": "Camera Storage", diff --git a/web/src/components/filter/CameraGroupSelector.tsx b/web/src/components/filter/CameraGroupSelector.tsx index 14845fdb8..6d86c57f8 100644 --- a/web/src/components/filter/CameraGroupSelector.tsx +++ b/web/src/components/filter/CameraGroupSelector.tsx @@ -990,7 +990,7 @@ export function CameraGroupEdit({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/components/overlay/CreateRoleDialog.tsx b/web/src/components/overlay/CreateRoleDialog.tsx index 0b10f1c9d..023d2b665 100644 --- a/web/src/components/overlay/CreateRoleDialog.tsx +++ b/web/src/components/overlay/CreateRoleDialog.tsx @@ -231,7 +231,7 @@ export default function CreateRoleDialog({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/components/overlay/CreateTriggerDialog.tsx b/web/src/components/overlay/CreateTriggerDialog.tsx index ef30c649d..1db934e3c 100644 --- a/web/src/components/overlay/CreateTriggerDialog.tsx +++ b/web/src/components/overlay/CreateTriggerDialog.tsx @@ -454,7 +454,7 @@ export default function CreateTriggerDialog({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/components/overlay/CreateUserDialog.tsx b/web/src/components/overlay/CreateUserDialog.tsx index 1ce32b61b..c0e17bb37 100644 --- a/web/src/components/overlay/CreateUserDialog.tsx +++ b/web/src/components/overlay/CreateUserDialog.tsx @@ -432,7 +432,7 @@ export default function CreateUserDialog({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/components/overlay/EditRoleCamerasDialog.tsx b/web/src/components/overlay/EditRoleCamerasDialog.tsx index 059de4a78..872226318 100644 --- a/web/src/components/overlay/EditRoleCamerasDialog.tsx +++ b/web/src/components/overlay/EditRoleCamerasDialog.tsx @@ -177,7 +177,7 @@ export default function EditRoleCamerasDialog({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/components/overlay/SetPasswordDialog.tsx b/web/src/components/overlay/SetPasswordDialog.tsx index 084a841e4..96c29ffdb 100644 --- a/web/src/components/overlay/SetPasswordDialog.tsx +++ b/web/src/components/overlay/SetPasswordDialog.tsx @@ -471,7 +471,7 @@ export default function SetPasswordDialog({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/components/overlay/detail/AnnotationSettingsPane.tsx b/web/src/components/overlay/detail/AnnotationSettingsPane.tsx index e6c4b19a2..06e0d80ce 100644 --- a/web/src/components/overlay/detail/AnnotationSettingsPane.tsx +++ b/web/src/components/overlay/detail/AnnotationSettingsPane.tsx @@ -192,7 +192,7 @@ export function AnnotationSettingsPane({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index f48a7d475..2e37f41a4 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -3,6 +3,7 @@ import { CameraConfig } from "@/types/frigateConfig"; import AutoUpdatingCameraImage from "../camera/AutoUpdatingCameraImage"; import ActivityIndicator from "../indicators/activity-indicator"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useResizeObserver } from "@/hooks/resize-observer"; import MSEPlayer from "./MsePlayer"; import JSMpegPlayer from "./JSMpegPlayer"; import { MdCircle } from "react-icons/md"; @@ -80,6 +81,7 @@ export default function LivePlayer({ const { t } = useTranslation(["components/player"]); const internalContainerRef = useRef(null); + const overlayRef = useRef(null); const cameraName = useCameraFriendlyName(cameraConfig); @@ -87,6 +89,10 @@ export default function LivePlayer({ const inDashboard = containerRef?.current == null; + const [overlayDimensions] = useResizeObserver(overlayRef); + const isCompact = + overlayDimensions.width > 0 && overlayDimensions.width < 280; + // stats const [stats, setStats] = useState({ @@ -317,7 +323,14 @@ export default function LivePlayer({ return (
{ + overlayRef.current = node; + if (cameraRef) { + cameraRef(node); + } else { + internalContainerRef.current = node; + } + }} data-camera={cameraConfig.name} className={cn( "relative flex w-full cursor-pointer justify-center outline", @@ -428,16 +441,18 @@ export default function LivePlayer({
{t("streamOffline.title")}
-

- - streamOffline.desc - -

+ {!isCompact && ( +

+ + streamOffline.desc + +

+ )}
@@ -448,16 +463,18 @@ export default function LivePlayer({

{t("streamOffline.title")}

-

- - streamOffline.desc - -

+ {!isCompact && ( +

+ + streamOffline.desc + +

+ )}
)} diff --git a/web/src/components/settings/CameraStreamingDialog.tsx b/web/src/components/settings/CameraStreamingDialog.tsx index b7f1979ea..4cabd2d86 100644 --- a/web/src/components/settings/CameraStreamingDialog.tsx +++ b/web/src/components/settings/CameraStreamingDialog.tsx @@ -391,7 +391,7 @@ export function CameraStreamingDialog({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/components/settings/MotionMaskEditPane.tsx b/web/src/components/settings/MotionMaskEditPane.tsx index 3857c4060..d613704cb 100644 --- a/web/src/components/settings/MotionMaskEditPane.tsx +++ b/web/src/components/settings/MotionMaskEditPane.tsx @@ -440,7 +440,7 @@ export default function MotionMaskEditPane({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/components/settings/ObjectMaskEditPane.tsx b/web/src/components/settings/ObjectMaskEditPane.tsx index 380c40be1..134976396 100644 --- a/web/src/components/settings/ObjectMaskEditPane.tsx +++ b/web/src/components/settings/ObjectMaskEditPane.tsx @@ -461,7 +461,7 @@ export default function ObjectMaskEditPane({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/components/settings/ZoneEditPane.tsx b/web/src/components/settings/ZoneEditPane.tsx index e52853972..20f4b097a 100644 --- a/web/src/components/settings/ZoneEditPane.tsx +++ b/web/src/components/settings/ZoneEditPane.tsx @@ -936,7 +936,7 @@ export default function ZoneEditPane({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/types/stats.ts b/web/src/types/stats.ts index 8b22849be..1046e0b47 100644 --- a/web/src/types/stats.ts +++ b/web/src/types/stats.ts @@ -86,6 +86,7 @@ export type StorageStats = { used: number; mount_type: string; min_shm?: number; + shm_frame_count?: number; }; export type PotentialProblem = { diff --git a/web/src/views/settings/EnrichmentsSettingsView.tsx b/web/src/views/settings/EnrichmentsSettingsView.tsx index 6aba50dd3..f066f2df9 100644 --- a/web/src/views/settings/EnrichmentsSettingsView.tsx +++ b/web/src/views/settings/EnrichmentsSettingsView.tsx @@ -603,7 +603,7 @@ export default function EnrichmentsSettingsView({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/views/settings/MotionTunerView.tsx b/web/src/views/settings/MotionTunerView.tsx index 25d5f1469..e95044ec1 100644 --- a/web/src/views/settings/MotionTunerView.tsx +++ b/web/src/views/settings/MotionTunerView.tsx @@ -299,7 +299,7 @@ export default function MotionTunerView({ > {isLoading ? (
- + {t("button.saving", { ns: "common" })}
) : ( diff --git a/web/src/views/system/StorageMetrics.tsx b/web/src/views/system/StorageMetrics.tsx index 2c222d6c3..d36200849 100644 --- a/web/src/views/system/StorageMetrics.tsx +++ b/web/src/views/system/StorageMetrics.tsx @@ -89,6 +89,35 @@ export default function StorageMetrics({ timezone, ); + const shmFrameLifetime = useMemo(() => { + if (!stats || !config) { + return undefined; + } + + const shmFrameCount = stats.service.storage["/dev/shm"]?.shm_frame_count; + + if (!shmFrameCount || shmFrameCount <= 0) { + return undefined; + } + + let maxCameraFps = 0; + + for (const [name, camStats] of Object.entries(stats.cameras)) { + if (config.cameras[name]?.enabled && camStats.camera_fps > 0) { + maxCameraFps = Math.max(maxCameraFps, camStats.camera_fps); + } + } + + if (maxCameraFps === 0) { + return undefined; + } + + return { + frames: shmFrameCount, + lifetime: Math.round((shmFrameCount / maxCameraFps) * 10) / 10, + }; + }, [stats, config]); + if (!cameraStorage || !stats || !totalStorage || !config) { return; } @@ -148,43 +177,68 @@ export default function StorageMetrics({
/dev/shm - {stats.service.storage["/dev/shm"]["total"] < - (stats.service.storage["/dev/shm"]["min_shm"] ?? 0) && ( - - - - - -
- {t("storage.shm.warning", { - total: stats.service.storage["/dev/shm"]["total"], - min_shm: stats.service.storage["/dev/shm"]["min_shm"], - })} -
- - {t("readTheDocumentation", { ns: "common" })} - - +
+ {shmFrameLifetime && ( + + + + + +
+ {t("storage.shm.frameLifetime.description", { + frames: shmFrameLifetime.frames, + lifetime: shmFrameLifetime.lifetime, + })}
-
- - - )} + + + )} + {stats.service.storage["/dev/shm"]["total"] < + (stats.service.storage["/dev/shm"]["min_shm"] ?? 0) && ( + + + + + +
+ {t("storage.shm.warning", { + total: stats.service.storage["/dev/shm"]["total"], + min_shm: stats.service.storage["/dev/shm"]["min_shm"], + })} +
+ + {t("readTheDocumentation", { ns: "common" })} + + +
+
+
+
+ )} +