diff --git a/frigate/stats/emitter.py b/frigate/stats/emitter.py index 42d4c16a8..2b34c7c4e 100644 --- a/frigate/stats/emitter.py +++ b/frigate/stats/emitter.py @@ -52,18 +52,66 @@ class StatsEmitter(threading.Thread): def get_stats_history( self, keys: Optional[list[str]] = None ) -> list[dict[str, Any]]: - """Get stats history.""" + """Get stats history. + + Supports dot-notation for nested keys to avoid returning large objects + when only specific subfields are needed. Handles two patterns: + + - Flat dict: "service.last_updated" returns {"service": {"last_updated": ...}} + - Dict-of-dicts: "cameras.camera_fps" returns each camera entry filtered + to only include "camera_fps" + """ if not keys: return self.stats_history + # Pre-parse keys into top-level keys and dot-notation fields + top_level_keys: list[str] = [] + nested_keys: dict[str, list[str]] = {} + + for k in keys: + if "." in k: + parent_key, child_key = k.split(".", 1) + nested_keys.setdefault(parent_key, []).append(child_key) + else: + top_level_keys.append(k) + selected_stats: list[dict[str, Any]] = [] for s in self.stats_history: - selected = {} + selected: dict[str, Any] = {} - for k in keys: + for k in top_level_keys: selected[k] = s.get(k) + for parent_key, child_keys in nested_keys.items(): + parent = s.get(parent_key) + + if not isinstance(parent, dict): + selected[parent_key] = parent + continue + + # Check if values are dicts (dict-of-dicts like cameras/detectors) + first_value = next(iter(parent.values()), None) + + if isinstance(first_value, dict): + # Filter each nested entry to only requested fields, + # omitting None values to preserve key-absence semantics + selected[parent_key] = { + entry_key: { + field: val + for field in child_keys + if (val := entry.get(field)) is not None + } + for entry_key, entry in parent.items() + } + else: + # Flat dict (like service) - pick individual fields + if parent_key not in selected: + selected[parent_key] = {} + + for child_key in child_keys: + selected[parent_key][child_key] = parent.get(child_key) + selected_stats.append(selected) return selected_stats diff --git a/frigate/stats/util.py b/frigate/stats/util.py index 99e9981bd..07b410ad2 100644 --- a/frigate/stats/util.py +++ b/frigate/stats/util.py @@ -498,4 +498,30 @@ def stats_snapshot( "pid": pid, } + # Embed cpu/mem stats into detectors, cameras, and processes + # so history consumers don't need the full cpu_usages dict + cpu_usages = stats.get("cpu_usages", {}) + + for det_stats in stats["detectors"].values(): + pid_str = str(det_stats.get("pid", "")) + usage = cpu_usages.get(pid_str, {}) + det_stats["cpu"] = usage.get("cpu") + det_stats["mem"] = usage.get("mem") + + for cam_stats in stats["cameras"].values(): + for pid_key, field in [ + ("ffmpeg_pid", "ffmpeg_cpu"), + ("capture_pid", "capture_cpu"), + ("pid", "detect_cpu"), + ]: + pid_str = str(cam_stats.get(pid_key, "")) + usage = cpu_usages.get(pid_str, {}) + cam_stats[field] = usage.get("cpu") + + for proc_stats in stats["processes"].values(): + pid_str = str(proc_stats.get("pid", "")) + usage = cpu_usages.get(pid_str, {}) + proc_stats["cpu"] = usage.get("cpu") + proc_stats["mem"] = usage.get("mem") + return stats diff --git a/web/src/components/graph/LineGraph.tsx b/web/src/components/graph/LineGraph.tsx index ad841dea2..cf2114a9c 100644 --- a/web/src/components/graph/LineGraph.tsx +++ b/web/src/components/graph/LineGraph.tsx @@ -2,7 +2,7 @@ import { useTheme } from "@/context/theme-provider"; import { useDateLocale } from "@/hooks/use-date-locale"; import { FrigateConfig } from "@/types/frigateConfig"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import Chart from "react-apexcharts"; import { isMobileOnly } from "react-device-detect"; import { useTranslation } from "react-i18next"; @@ -17,6 +17,7 @@ type CameraLineGraphProps = { dataLabels: string[]; updateTimes: number[]; data: ApexAxisChartSeries; + isActive?: boolean; }; export function CameraLineGraph({ graphId, @@ -24,6 +25,7 @@ export function CameraLineGraph({ dataLabels, updateTimes, data, + isActive = true, }: CameraLineGraphProps) { const { t } = useTranslation(["views/system", "common"]); const { data: config } = useSWR("config", { @@ -134,6 +136,16 @@ export function CameraLineGraph({ ApexCharts.exec(graphId, "updateOptions", options, true, true); }, [graphId, options]); + const hasBeenActive = useRef(isActive); + useEffect(() => { + if (isActive && hasBeenActive.current === false) { + ApexCharts.exec(graphId, "updateSeries", data, true); + } + hasBeenActive.current = isActive; + // only replay animation on visibility change, not data updates + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isActive, graphId]); + return (
{lastValues && ( @@ -166,6 +178,7 @@ type EventsPerSecondLineGraphProps = { name: string; updateTimes: number[]; data: ApexAxisChartSeries; + isActive?: boolean; }; export function EventsPerSecondsLineGraph({ graphId, @@ -173,6 +186,7 @@ export function EventsPerSecondsLineGraph({ name, updateTimes, data, + isActive = true, }: EventsPerSecondLineGraphProps) { const { data: config } = useSWR("config", { revalidateOnFocus: false, @@ -277,6 +291,16 @@ export function EventsPerSecondsLineGraph({ ApexCharts.exec(graphId, "updateOptions", options, true, true); }, [graphId, options]); + const hasBeenActive = useRef(isActive); + useEffect(() => { + if (isActive && hasBeenActive.current === false) { + ApexCharts.exec(graphId, "updateSeries", data, true); + } + hasBeenActive.current = isActive; + // only replay animation on visibility change, not data updates + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isActive, graphId]); + return (
diff --git a/web/src/components/graph/SystemGraph.tsx b/web/src/components/graph/SystemGraph.tsx index eaf4ae226..bf13417d0 100644 --- a/web/src/components/graph/SystemGraph.tsx +++ b/web/src/components/graph/SystemGraph.tsx @@ -3,7 +3,7 @@ import { useDateLocale } from "@/hooks/use-date-locale"; import { FrigateConfig } from "@/types/frigateConfig"; import { Threshold } from "@/types/graph"; import { formatUnixTimestampToDateTime } from "@/utils/dateUtil"; -import { useCallback, useEffect, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import Chart from "react-apexcharts"; import { isMobileOnly } from "react-device-detect"; import { useTranslation } from "react-i18next"; @@ -16,6 +16,7 @@ type ThresholdBarGraphProps = { threshold: Threshold; updateTimes: number[]; data: ApexAxisChartSeries; + isActive?: boolean; }; export function ThresholdBarGraph({ graphId, @@ -24,6 +25,7 @@ export function ThresholdBarGraph({ threshold, updateTimes, data, + isActive = true, }: ThresholdBarGraphProps) { const displayName = name || data[0]?.name || ""; const { data: config } = useSWR("config", { @@ -173,17 +175,29 @@ export function ThresholdBarGraph({ return data; } - const copiedData = [...data]; + const dataPointCount = data[0].data.length; const fakeData = []; - for (let i = data.length; i < 30; i++) { + for (let i = dataPointCount; i < 30; i++) { fakeData.push({ x: i - 30, y: 0 }); } - // @ts-expect-error data types are not obvious - copiedData[0].data = [...fakeData, ...data[0].data]; - return copiedData; + const paddedFirst = { + ...data[0], + data: [...fakeData, ...data[0].data], + }; + return [paddedFirst, ...data.slice(1)] as ApexAxisChartSeries; }, [data]); + const hasBeenActive = useRef(isActive); + useEffect(() => { + if (isActive && hasBeenActive.current === false) { + ApexCharts.exec(graphId, "updateSeries", chartData, true); + } + hasBeenActive.current = isActive; + // only replay animation on visibility change, not data updates + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isActive, graphId]); + return (
diff --git a/web/src/pages/System.tsx b/web/src/pages/System.tsx index 61e4d8368..c7c543e7f 100644 --- a/web/src/pages/System.tsx +++ b/web/src/pages/System.tsx @@ -1,6 +1,6 @@ import useSWR from "swr"; import { FrigateStats } from "@/types/stats"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import TimeAgo from "@/components/dynamic/TimeAgo"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { isDesktop, isMobile } from "react-device-detect"; @@ -49,7 +49,17 @@ function System() { setPage, 100, ); - const [lastUpdated, setLastUpdated] = useState(Date.now() / 1000); + const [lastUpdated, setLastUpdated] = useState( + Math.floor(Date.now() / 1000), + ); + + // Track which tabs have been visited so we can keep them mounted after first visit. + // Using a ref updated during render avoids extra render cycles from state/effects. + const visitedTabsRef = useRef(new Set()); + if (page) { + visitedTabsRef.current.add(page); + } + const visitedTabs = visitedTabsRef.current; useEffect(() => { if (pageToggle) { @@ -116,24 +126,37 @@ function System() {
)}
- {page == "general" && ( - + {visitedTabs.has("general") && ( +
+ +
)} - {page == "enrichments" && ( - + {metrics.includes("enrichments") && visitedTabs.has("enrichments") && ( +
+ +
)} - {page == "storage" && } - {page == "cameras" && ( - + {visitedTabs.has("storage") && ( +
+ +
+ )} + {visitedTabs.has("cameras") && ( +
+ +
)}
); diff --git a/web/src/types/stats.ts b/web/src/types/stats.ts index c4b811185..0bd4ebde3 100644 --- a/web/src/types/stats.ts +++ b/web/src/types/stats.ts @@ -28,6 +28,9 @@ export type CameraStats = { expected_fps: number; reconnects_last_hour: number; stalls_last_hour: number; + ffmpeg_cpu?: string; + capture_cpu?: string; + detect_cpu?: string; }; export type CpuStats = { @@ -42,6 +45,8 @@ export type DetectorStats = { inference_speed: number; pid: number; temperature?: number; + cpu?: string; + mem?: string; }; export type EmbeddingsStats = { @@ -53,6 +58,8 @@ export type EmbeddingsStats = { export type ExtraProcessStats = { pid: number; + cpu?: string; + mem?: string; }; export type GpuStats = { diff --git a/web/src/views/system/CameraMetrics.tsx b/web/src/views/system/CameraMetrics.tsx index b6c5be4fa..4e524a743 100644 --- a/web/src/views/system/CameraMetrics.tsx +++ b/web/src/views/system/CameraMetrics.tsx @@ -5,7 +5,14 @@ import { ConnectionQualityIndicator } from "@/components/camera/ConnectionQualit import { Skeleton } from "@/components/ui/skeleton"; import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateStats } from "@/types/stats"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { + Fragment, + startTransition, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { MdInfo } from "react-icons/md"; import { Tooltip, @@ -20,10 +27,12 @@ import { resolveCameraName } from "@/hooks/use-camera-friendly-name"; type CameraMetricsProps = { lastUpdated: number; setLastUpdated: (last: number) => void; + isActive: boolean; }; export default function CameraMetrics({ lastUpdated, setLastUpdated, + isActive, }: CameraMetricsProps) { const { data: config } = useSWR("config"); const { t } = useTranslation(["views/system"]); @@ -39,11 +48,11 @@ export default function CameraMetrics({ // stats - const { data: initialStats } = useSWR( + const { data: initialStats, mutate: refreshStats } = useSWR( [ "stats/history", { - keys: "cpu_usages,cameras,camera_fps,detection_fps,skipped_fps,service", + keys: "cameras.camera_fps,cameras.detection_fps,cameras.skipped_fps,cameras.ffmpeg_cpu,cameras.capture_cpu,cameras.detect_cpu,cameras.connection_quality,cameras.expected_fps,cameras.reconnects_last_hour,cameras.stalls_last_hour,camera_fps,detection_fps,skipped_fps,service.last_updated", }, ], { @@ -60,19 +69,38 @@ export default function CameraMetrics({ } if (statsHistory.length == 0) { - setStatsHistory(initialStats); + startTransition(() => setStatsHistory(initialStats)); return; } - if (!updatedStats) { + if (!isActive || !updatedStats) { return; } if (updatedStats.service.last_updated > lastUpdated) { setStatsHistory([...statsHistory.slice(1), updatedStats]); - setLastUpdated(Date.now() / 1000); + setLastUpdated(updatedStats.service.last_updated); } - }, [initialStats, updatedStats, statsHistory, lastUpdated, setLastUpdated]); + }, [ + initialStats, + updatedStats, + statsHistory, + lastUpdated, + setLastUpdated, + isActive, + ]); + + useEffect(() => { + if (isActive && statsHistory.length > 0) { + refreshStats().then((freshStats) => { + if (freshStats && freshStats.length > 0) { + setStatsHistory(freshStats); + } + }); + } + // only re-fetch when tab becomes active, not on data changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isActive]); // timestamps @@ -168,15 +196,15 @@ export default function CameraMetrics({ series[key]["ffmpeg"].data.push({ x: statsIdx, - y: stats.cpu_usages[camStats.ffmpeg_pid.toString()]?.cpu ?? 0.0, + y: camStats.ffmpeg_cpu ?? "0", }); series[key]["capture"].data.push({ x: statsIdx, - y: stats.cpu_usages[camStats.capture_pid?.toString()]?.cpu ?? 0, + y: camStats.capture_cpu ?? "0", }); series[key]["detect"].data.push({ x: statsIdx, - y: stats.cpu_usages[camStats.pid?.toString()]?.cpu, + y: camStats.detect_cpu ?? "0", }); }); }); @@ -261,6 +289,7 @@ export default function CameraMetrics({ dataLabels={["camera", "detect", "skipped"]} updateTimes={updateTimes} data={overallFpsSeries} + isActive={isActive} />
) : ( @@ -272,10 +301,9 @@ export default function CameraMetrics({ Object.values(config.cameras).map((camera) => { if (camera.enabled) { return ( - <> + {probeCameraName == camera.name && (
) : ( @@ -363,6 +392,7 @@ export default function CameraMetrics({ data={Object.values( cameraFpsSeries[camera.name] || {}, )} + isActive={isActive} /> ) : ( @@ -370,7 +400,7 @@ export default function CameraMetrics({ )} - + ); } diff --git a/web/src/views/system/EnrichmentMetrics.tsx b/web/src/views/system/EnrichmentMetrics.tsx index 252d63ebc..fbd30d705 100644 --- a/web/src/views/system/EnrichmentMetrics.tsx +++ b/web/src/views/system/EnrichmentMetrics.tsx @@ -1,6 +1,12 @@ import useSWR from "swr"; import { FrigateStats } from "@/types/stats"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { + startTransition, + useCallback, + useEffect, + useMemo, + useState, +} from "react"; import { useFrigateStats } from "@/api/ws"; import { EmbeddingThreshold, GenAIThreshold, Threshold } from "@/types/graph"; import { Skeleton } from "@/components/ui/skeleton"; @@ -12,16 +18,18 @@ import { EventsPerSecondsLineGraph } from "@/components/graph/LineGraph"; type EnrichmentMetricsProps = { lastUpdated: number; setLastUpdated: (last: number) => void; + isActive: boolean; }; export default function EnrichmentMetrics({ lastUpdated, setLastUpdated, + isActive, }: EnrichmentMetricsProps) { // stats const { t } = useTranslation(["views/system"]); - const { data: initialStats } = useSWR( - ["stats/history", { keys: "embeddings,service" }], + const { data: initialStats, mutate: refreshStats } = useSWR( + ["stats/history", { keys: "embeddings,service.last_updated" }], { revalidateOnFocus: false, }, @@ -36,19 +44,38 @@ export default function EnrichmentMetrics({ } if (statsHistory.length == 0) { - setStatsHistory(initialStats); + startTransition(() => setStatsHistory(initialStats)); return; } - if (!updatedStats) { + if (!isActive || !updatedStats) { return; } if (updatedStats.service.last_updated > lastUpdated) { setStatsHistory([...statsHistory.slice(1), updatedStats]); - setLastUpdated(Date.now() / 1000); + setLastUpdated(updatedStats.service.last_updated); } - }, [initialStats, updatedStats, statsHistory, lastUpdated, setLastUpdated]); + }, [ + initialStats, + updatedStats, + statsHistory, + lastUpdated, + setLastUpdated, + isActive, + ]); + + useEffect(() => { + if (isActive && statsHistory.length > 0) { + refreshStats().then((freshStats) => { + if (freshStats && freshStats.length > 0) { + setStatsHistory(freshStats); + } + }); + } + // only re-fetch when tab becomes active, not on data changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isActive]); const getThreshold = useCallback((key: string) => { if (key.includes("description")) { @@ -205,6 +232,7 @@ export default function EnrichmentMetrics({ threshold={group.speedSeries.metrics} updateTimes={updateTimes} data={[group.speedSeries]} + isActive={isActive} /> )} {group.eventsSeries && ( @@ -215,6 +243,7 @@ export default function EnrichmentMetrics({ name={t("enrichments.infPerSecond")} updateTimes={updateTimes} data={[group.eventsSeries]} + isActive={isActive} /> )} diff --git a/web/src/views/system/GeneralMetrics.tsx b/web/src/views/system/GeneralMetrics.tsx index fc7410b5a..009f940e2 100644 --- a/web/src/views/system/GeneralMetrics.tsx +++ b/web/src/views/system/GeneralMetrics.tsx @@ -1,6 +1,6 @@ import useSWR from "swr"; import { FrigateStats, GpuInfo } from "@/types/stats"; -import { useEffect, useMemo, useState } from "react"; +import { startTransition, useEffect, useMemo, useState } from "react"; import { useFrigateStats } from "@/api/ws"; import { DetectorCpuThreshold, @@ -26,10 +26,12 @@ import { CiCircleAlert } from "react-icons/ci"; type GeneralMetricsProps = { lastUpdated: number; setLastUpdated: (last: number) => void; + isActive: boolean; }; export default function GeneralMetrics({ lastUpdated, setLastUpdated, + isActive, }: GeneralMetricsProps) { // extra info const { t } = useTranslation(["views/system"]); @@ -37,10 +39,12 @@ export default function GeneralMetrics({ // stats - const { data: initialStats } = useSWR( + const { data: initialStats, mutate: refreshStats } = useSWR( [ "stats/history", - { keys: "cpu_usages,detectors,gpu_usages,npu_usages,processes,service" }, + { + keys: "detectors.inference_speed,detectors.temperature,detectors.cpu,detectors.mem,gpu_usages,npu_usages,processes.cpu,processes.mem,service.last_updated", + }, ], { revalidateOnFocus: false, @@ -56,19 +60,38 @@ export default function GeneralMetrics({ } if (statsHistory.length == 0) { - setStatsHistory(initialStats); + startTransition(() => setStatsHistory(initialStats)); return; } - if (!updatedStats) { + if (!isActive || !updatedStats) { return; } if (updatedStats.service.last_updated > lastUpdated) { setStatsHistory([...statsHistory.slice(1), updatedStats]); - setLastUpdated(Date.now() / 1000); + setLastUpdated(updatedStats.service.last_updated); } - }, [initialStats, updatedStats, statsHistory, lastUpdated, setLastUpdated]); + }, [ + initialStats, + updatedStats, + statsHistory, + lastUpdated, + setLastUpdated, + isActive, + ]); + + useEffect(() => { + if (isActive && statsHistory.length > 0) { + refreshStats().then((freshStats) => { + if (freshStats && freshStats.length > 0) { + setStatsHistory(freshStats); + } + }); + } + // only re-fetch when tab becomes active, not on data changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isActive]); const [canGetGpuInfo, gpuType] = useMemo<[boolean, GpuInfo]>(() => { let vaCount = 0; @@ -181,7 +204,7 @@ export default function GeneralMetrics({ series[key] = { name: key, data: [] }; } - const data = stats.cpu_usages[detStats.pid.toString()]?.cpu; + const data = detStats.cpu; if (data != undefined) { series[key].data.push({ @@ -213,10 +236,12 @@ export default function GeneralMetrics({ series[key] = { name: key, data: [] }; } - series[key].data.push({ - x: statsIdx + 1, - y: stats.cpu_usages[detStats.pid.toString()].mem, - }); + if (detStats.mem != undefined) { + series[key].data.push({ + x: statsIdx + 1, + y: detStats.mem, + }); + } }); }); return Object.values(series); @@ -581,22 +606,18 @@ export default function GeneralMetrics({ } Object.entries(stats.processes).forEach(([key, procStats]) => { - if (procStats.pid.toString() in stats.cpu_usages) { - if (!(key in series)) { - series[key] = { - name: t(`general.otherProcesses.series.${key}`), - data: [], - }; - } + if (!(key in series)) { + series[key] = { + name: t(`general.otherProcesses.series.${key}`), + data: [], + }; + } - const data = stats.cpu_usages[procStats.pid.toString()]?.cpu; - - if (data != undefined) { - series[key].data.push({ - x: statsIdx + 1, - y: data, - }); - } + if (procStats.cpu != undefined) { + series[key].data.push({ + x: statsIdx + 1, + y: procStats.cpu, + }); } }); }); @@ -618,22 +639,18 @@ export default function GeneralMetrics({ } Object.entries(stats.processes).forEach(([key, procStats]) => { - if (procStats.pid.toString() in stats.cpu_usages) { - if (!(key in series)) { - series[key] = { - name: t(`general.otherProcesses.series.${key}`), - data: [], - }; - } + if (!(key in series)) { + series[key] = { + name: t(`general.otherProcesses.series.${key}`), + data: [], + }; + } - const data = stats.cpu_usages[procStats.pid.toString()]?.mem; - - if (data) { - series[key].data.push({ - x: statsIdx + 1, - y: data, - }); - } + if (procStats.mem) { + series[key].data.push({ + x: statsIdx + 1, + y: procStats.mem, + }); } }); }); @@ -670,6 +687,7 @@ export default function GeneralMetrics({ threshold={InferenceThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -692,6 +710,7 @@ export default function GeneralMetrics({ threshold={DetectorTempThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -730,6 +749,7 @@ export default function GeneralMetrics({ threshold={DetectorCpuThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -748,6 +768,7 @@ export default function GeneralMetrics({ threshold={DetectorMemThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -840,6 +861,7 @@ export default function GeneralMetrics({ threshold={GPUUsageThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -862,6 +884,7 @@ export default function GeneralMetrics({ threshold={GPUMemThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -886,6 +909,7 @@ export default function GeneralMetrics({ threshold={GPUMemThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -910,6 +934,7 @@ export default function GeneralMetrics({ threshold={GPUMemThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -934,6 +959,7 @@ export default function GeneralMetrics({ threshold={GPUMemThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -958,6 +984,7 @@ export default function GeneralMetrics({ threshold={DetectorTempThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -983,6 +1010,7 @@ export default function GeneralMetrics({ threshold={GPUUsageThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -1005,6 +1033,7 @@ export default function GeneralMetrics({ threshold={DetectorTempThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -1038,6 +1067,7 @@ export default function GeneralMetrics({ threshold={DetectorCpuThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} @@ -1057,6 +1087,7 @@ export default function GeneralMetrics({ threshold={DetectorMemThreshold} updateTimes={updateTimes} data={[series]} + isActive={isActive} /> ))} diff --git a/web/src/views/system/StorageMetrics.tsx b/web/src/views/system/StorageMetrics.tsx index d36200849..84afe20c8 100644 --- a/web/src/views/system/StorageMetrics.tsx +++ b/web/src/views/system/StorageMetrics.tsx @@ -1,7 +1,7 @@ import { CombinedStorageGraph } from "@/components/graph/CombinedStorageGraph"; import { StorageGraph } from "@/components/graph/StorageGraph"; import { FrigateStats } from "@/types/stats"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { Popover, PopoverContent, @@ -56,9 +56,14 @@ export default function StorageMetrics({ Object.values(cameraStorage).forEach( (cam) => (totalStorage.camera += cam.usage), ); - setLastUpdated(Date.now() / 1000); return totalStorage; - }, [cameraStorage, stats, setLastUpdated]); + }, [cameraStorage, stats]); + + useEffect(() => { + if (totalStorage) { + setLastUpdated(Math.floor(Date.now() / 1000)); + } + }, [totalStorage, setLastUpdated]); // recordings summary