diff --git a/Dockerfile b/Dockerfile
index 44805b2..0c51d51 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
# Build commands:
-# - $VERSION=0.7
+# - $VERSION=0.8
# - rm build -r -Force ; rm ./node_modules/.cache/babel-loader -r -Force ; yarn build
# - docker build --pull --rm -t oncharterliz/multi-frigate:latest -t oncharterliz/multi-frigate:$VERSION "."
# - docker image push --all-tags oncharterliz/multi-frigate
diff --git a/src/locales/en.ts b/src/locales/en.ts
index ce5b35f..a416a63 100644
--- a/src/locales/en.ts
+++ b/src/locales/en.ts
@@ -1,11 +1,25 @@
const en = {
+ cameraStatTable: {
+ process: 'Process',
+ pid: 'PID',
+ fps: 'FPS',
+ cpu: 'CPU %',
+ memory: 'Memory %'
+ },
+ hostMenu: {
+ editConfig: 'Edit config',
+ restart: 'Restart',
+ system: 'System',
+ storage: 'Storage',
+ },
header: {
home: 'Main',
settings: 'Settings',
recordings: 'Recordings',
hostsConfig: 'Frigate servers',
acessSettings: 'Access settings',
- }, hostArr: {
+ },
+ hostArr: {
host: 'Host',
name: 'Host name',
url: 'Address',
diff --git a/src/locales/ru.ts b/src/locales/ru.ts
index 25e31ab..aab9af5 100644
--- a/src/locales/ru.ts
+++ b/src/locales/ru.ts
@@ -1,4 +1,10 @@
const ru = {
+ hostMenu: {
+ editConfig: 'Редакт. конфиг.',
+ restart: 'Перезагрузка',
+ system: 'Система',
+ storage: 'Хранилище',
+ },
header: {
home: 'Главная',
settings: 'Настройки',
diff --git a/src/pages/FrigateHostsPage.tsx b/src/pages/FrigateHostsPage.tsx
index ac434db..2108353 100644
--- a/src/pages/FrigateHostsPage.tsx
+++ b/src/pages/FrigateHostsPage.tsx
@@ -11,7 +11,7 @@ import { frigateApi, frigateQueryKeys } from '../services/frigate.proxy/frigate.
import { GetFrigateHost, deleteFrigateHostSchema, putFrigateHostSchema } from '../services/frigate.proxy/frigate.schema';
import CenterLoader from '../shared/components/loaders/CenterLoader';
import { isProduction } from '../shared/env.const';
-import FrigateHostsTable from '../widgets/FrigateHostsTable';
+import FrigateHostsTable from '../widgets/hosts.table/FrigateHostsTable';
import Forbidden from './403';
import RetryErrorPage from './RetryErrorPage';
diff --git a/src/pages/HostSystemPage.tsx b/src/pages/HostSystemPage.tsx
index a5b8345..8ed242a 100644
--- a/src/pages/HostSystemPage.tsx
+++ b/src/pages/HostSystemPage.tsx
@@ -1,8 +1,19 @@
-import React, { useContext, useEffect, useRef } from 'react';
+import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react';
import { Context } from '..';
import { useAdminRole } from '../hooks/useAdminRole';
import Forbidden from './403';
import { observer } from 'mobx-react-lite';
+import { useLocation, useNavigate, useParams } from 'react-router-dom';
+import { useQuery } from '@tanstack/react-query';
+import { frigateApi, frigateQueryKeys, mapHostToHostname, proxyApi } from '../services/frigate.proxy/frigate.api';
+import CenterLoader from '../shared/components/loaders/CenterLoader';
+import RetryErrorPage from './RetryErrorPage';
+import { Flex } from '@mantine/core';
+import FrigateCamerasStateTable, { CameraItem, ProcessType } from '../widgets/camera.stat.table/FrigateCameraStateTable';
+
+export const hostSystemPageQuery = {
+ hostId: 'hostId',
+}
const HostSystemPage = () => {
const executed = useRef(false)
@@ -18,12 +29,58 @@ const HostSystemPage = () => {
}
}, [sideBarsStore])
+ const location = useLocation()
+ const queryParams = useMemo(() => {
+ return new URLSearchParams(location.search);
+ }, [location.search])
+
+ let { id: paramHostId } = useParams<'id'>()
+
+ const { data, isError, isPending, refetch } = useQuery({
+ queryKey: [frigateQueryKeys.getHostStats, paramHostId],
+ queryFn: async () => {
+ if (!paramHostId) return null
+ const host = await frigateApi.getHost(paramHostId)
+ const hostName = mapHostToHostname(host)
+ if (!hostName) return null
+ return proxyApi.getHostStats(hostName)
+ }
+ })
+
+ const mapCameraData = useCallback(() => {
+ if (!data) return []
+ return Object.entries(data.cameras).flatMap(([name, stats]) => {
+ return (['Ffmpeg', 'Capture', 'Detect'] as ProcessType[]).map(type => {
+ const pid = type === ProcessType.Ffmpeg ? stats.ffmpeg_pid :
+ type === ProcessType.Capture ? stats.capture_pid : stats.pid;
+ const fps = type === ProcessType.Ffmpeg ? stats.camera_fps :
+ type === ProcessType.Capture ? stats.process_fps : stats.detection_fps;
+ const cpu = data.cpu_usages[pid]?.cpu;
+ const mem = data.cpu_usages[pid]?.mem;
+
+ return {
+ cameraName: name,
+ process: type,
+ pid: pid,
+ fps: fps,
+ cpu: cpu ?? 0,
+ mem: mem ?? 0,
+ };
+ });
+ });
+ }, [data]);
+
if (!isAdmin) return
+ if (isPending) return
+ if (isError) return
+ if (!paramHostId || !data) return null
+
+ const mappedCameraStat: CameraItem[] = mapCameraData()
return (
-
- System Page - NOT YET IMPLEMENTED
-
+
+
+
);
};
diff --git a/src/services/frigate.proxy/frigate.api.ts b/src/services/frigate.proxy/frigate.api.ts
index 574d35b..62e87b3 100644
--- a/src/services/frigate.proxy/frigate.api.ts
+++ b/src/services/frigate.proxy/frigate.api.ts
@@ -10,6 +10,7 @@ import { RecordSummary } from "../../types/record";
import { EventFrigate } from "../../types/event";
import { keycloakConfig } from "../..";
import { getResolvedTimeZone } from "../../shared/utils/dateUtil";
+import { FrigateStats } from "../../types/frigateStats";
export const getToken = (): string | undefined => {
@@ -171,7 +172,8 @@ export const proxyApi = {
getExportedVideoList: (hostName: string) => instanceApi.get(`proxy/${hostName}/exports/`).then(res => res.data),
getVideoUrl: (hostName: string, fileName: string) => `${proxyPrefix}${hostName}/exports/${fileName}`,
// filename example Home_1_Backyard_2024_02_26_16_25__2024_02_26_16_26.mp4
- deleteExportedVideo: (hostName: string, videoName: string) => instanceApi.delete(`proxy/${hostName}/api/export/${videoName}`).then(res => res.data)
+ deleteExportedVideo: (hostName: string, videoName: string) => instanceApi.delete(`proxy/${hostName}/api/export/${videoName}`).then(res => res.data),
+ getHostStats: (hostName: string) => instanceApi.get(`proxy/${hostName}/api/stats`).then( res => res.data),
}
export const mapCamerasFromConfig = (config: FrigateConfig): string[] => {
@@ -194,6 +196,7 @@ export const frigateQueryKeys = {
getCameraWHost: 'camera-frigate-host',
getCameraByHostId: 'camera-by-hostId',
getHostConfig: 'host-config',
+ getHostStats: 'host-stats',
getRecordingsSummary: 'recordings-frigate-summary',
getRecordings: 'recordings-frigate',
getEvents: 'events-frigate',
diff --git a/src/shared/components/accordion/CameraAccordion.tsx b/src/shared/components/accordion/CameraAccordion.tsx
index 56224ed..d2d7257 100644
--- a/src/shared/components/accordion/CameraAccordion.tsx
+++ b/src/shared/components/accordion/CameraAccordion.tsx
@@ -64,6 +64,9 @@ const CameraAccordion = () => {
if (!data || !camera) return null
+ if (!isProduction) console.log('camera', camera)
+ if (!isProduction) console.log('data', data)
+ if (!isProduction) console.log('hostName', hostName)
if (!isProduction) console.log('CameraAccordion rendered')
return (
diff --git a/src/shared/components/buttons/AccordionShareButton.tsx b/src/shared/components/buttons/AccordionShareButton.tsx
index c7ececc..22f4d44 100644
--- a/src/shared/components/buttons/AccordionShareButton.tsx
+++ b/src/shared/components/buttons/AccordionShareButton.tsx
@@ -17,6 +17,8 @@ const AccordionShareButton = ({
const url = recordUrl ? `${routesPath.PLAYER_PATH}?link=${encodeURIComponent(recordUrl)}` : ''
const handleShare = async () => {
+ if (!isProduction) console.log('canShare', canShare)
+ if (!isProduction) console.log('shared URL', url)
if (canShare && url) {
try {
await navigator.share({ url });
diff --git a/src/shared/components/filters/RecordingsHostFilter.tsx b/src/shared/components/filters/RecordingsHostFilter.tsx
index 75576a7..6495b60 100644
--- a/src/shared/components/filters/RecordingsHostFilter.tsx
+++ b/src/shared/components/filters/RecordingsHostFilter.tsx
@@ -5,6 +5,7 @@ import { Context } from '../../..';
import { frigateApi, frigateQueryKeys } from '../../../services/frigate.proxy/frigate.api';
import HostSelect from './HostSelect';
import { useTranslation } from 'react-i18next';
+import { isProduction } from '../../env.const';
const RecordingsHostFilter = () => {
const { t } = useTranslation()
@@ -17,6 +18,7 @@ const RecordingsHostFilter = () => {
const handleSelect = (value: string) => {
+ if (!isProduction) console.log('handleSelect value', value)
const host = hosts?.find(host => host.id === value)
if (!host) {
recStore.filteredHost = undefined
@@ -39,11 +41,11 @@ const RecordingsHostFilter = () => {
return (
);
};
diff --git a/src/shared/components/menu/HostSettingsMenu.tsx b/src/shared/components/menu/HostSettingsMenu.tsx
index 8f322f4..cf01d95 100644
--- a/src/shared/components/menu/HostSettingsMenu.tsx
+++ b/src/shared/components/menu/HostSettingsMenu.tsx
@@ -1,9 +1,9 @@
-import { Button, Menu, rem, Text } from '@mantine/core';
-import { IconEdit, IconGraph, IconMessageCircle, IconRotateClockwise, IconServer, IconSettings } from '@tabler/icons-react';
-import React from 'react';
+import { Button, Menu, rem } from '@mantine/core';
+import { IconEdit, IconGraph, IconRotateClockwise, IconServer, IconSettings } from '@tabler/icons-react';
+import { useMutation } from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { routesPath } from '../../../router/routes.path';
-import { useMutation } from '@tanstack/react-query';
import { mapHostToHostname, proxyApi } from '../../../services/frigate.proxy/frigate.api';
import { GetFrigateHost } from '../../../services/frigate.proxy/frigate.schema';
@@ -12,6 +12,7 @@ interface HostSettingsMenuProps {
}
const HostSettingsMenu = ({ host }: HostSettingsMenuProps) => {
+ const {t} = useTranslation()
const navigate = useNavigate()
const mutation = useMutation({
mutationFn: (hostName: string) => proxyApi.getHostRestart(hostName)
@@ -44,23 +45,23 @@ const HostSettingsMenu = ({ host }: HostSettingsMenuProps) => {
}>
- Edit Config
+ {t('hostMenu.editConfig')}
}>
- Restart
+ {t('hostMenu.restart')}
}>
- System
+ {t('hostMenu.system')}
}>
- Storage
+ {t('hostMenu.storage')}
diff --git a/src/shared/utils/sort.array.ts b/src/shared/utils/sort.array.ts
index 07ecc01..e5a8f48 100644
--- a/src/shared/utils/sort.array.ts
+++ b/src/shared/utils/sort.array.ts
@@ -1,19 +1,33 @@
- /**
- * Function get array and sort it by index of key
- * @param uniqueValue Name of table head, to change image on it
- * @param objectIndex Index of head, must be equal to idex of array object
- * @param arrayData Array data
- * @param reverse If you need to reverse array
- * @returns uniqueValue and sorted array
- */
+/**
+ * Function get array and sort it by index of key
+ * @param uniqueValue Name of table head, to change image on it
+ * @param objectIndex Index of head, must be equal to idex of array object
+ * @param arrayData Array data
+ * @param reverse If you need to reverse array
+ * @returns uniqueValue and sorted array
+ */
export function sortArrayByObjectIndex(
- objectIndex: number,
- arrayData: T[],
- callBack: (arrayData: T[], key: string | number | symbol) => void,
- reverse?: boolean,
- ) {
- if (arrayData.length === 0) throw Error('handleSort failed, array is empty')
- const keys = Object.keys(arrayData[0])
- const key = keys[objectIndex]
- callBack(arrayData, key as keyof T)
- }
\ No newline at end of file
+ objectIndex: number,
+ arrayData: T[],
+ callBack: (arrayData: T[], key: string | number | symbol) => void,
+ reverse?: boolean,
+) {
+ if (arrayData.length === 0) throw Error('handleSort failed, array is empty')
+ const keys = Object.keys(arrayData[0])
+ const key = keys[objectIndex]
+ callBack(arrayData, key as keyof T)
+}
+
+export function sortByKey(array: T[], key: K): T[] {
+ return array.sort((a, b) => {
+ let valueA = a[key];
+ let valueB = b[key];
+
+ const stringValueA = String(valueA).toLowerCase();
+ const stringValueB = String(valueB).toLowerCase();
+
+ if (stringValueA < stringValueB) return -1;
+ if (stringValueA > stringValueB) return 1;
+ return 0;
+ });
+}
\ No newline at end of file
diff --git a/src/types/frigateStats.ts b/src/types/frigateStats.ts
new file mode 100644
index 0000000..3b42888
--- /dev/null
+++ b/src/types/frigateStats.ts
@@ -0,0 +1,88 @@
+export interface FrigateStats {
+ cameras: {
+ [cameraName: string]: CameraStat
+ }
+ cpu_usages: {
+ [processId: string]: ProcessStat
+ }
+ detection_fps: number
+ detectors: {
+ [detectorName: string]: DetectorStat
+ }
+ gpu_usages: {
+ [gpuName: string] : GpuStat
+ }
+ processes: Processes
+ service: Service
+}
+
+export interface CameraStat {
+ audio_dBFS: number
+ audio_rms: number
+ camera_fps: number // Ffmpeg
+ capture_pid: number // Capture PID
+ detection_enabled: number // Detect
+ detection_fps: number // Detect
+ ffmpeg_pid: number // Ffmpeg PID
+ pid: number // Detect PID
+ process_fps: number // Capture
+ skipped_fps: number // Detect
+}
+
+export interface ProcessStat {
+ cmdline: string
+ cpu: string
+ cpu_average: string
+ mem: string
+}
+
+export interface DetectorStat {
+ detection_start: number
+ inference_speed: number
+ pid: number
+}
+
+export interface GpuStat {
+ dec: string
+ enc: string
+ gpu: string
+ mem: string
+}
+
+export interface Processes {
+ go2rtc: Go2rtc
+ logger: Logger
+ recording: Recording
+}
+
+export interface Go2rtc {
+ pid: number
+}
+
+export interface Logger {
+ pid: number
+}
+
+export interface Recording {
+ pid: number
+}
+
+export interface Service {
+ last_updated: number
+ latest_version: string
+ storage: {
+ [storagePath: string] : StorageStat
+ }
+ temperatures: Temperatures
+ uptime: number
+ version: string
+}
+
+export interface StorageStat {
+ free: number
+ mount_type: string
+ total: number
+ used: number
+}
+
+export interface Temperatures { }
diff --git a/src/widgets/SelectedCameraList.tsx b/src/widgets/SelectedCameraList.tsx
index 05ae59a..2541e85 100644
--- a/src/widgets/SelectedCameraList.tsx
+++ b/src/widgets/SelectedCameraList.tsx
@@ -30,7 +30,9 @@ const SelectedCameraList = () => {
if (cameraPending) return
if (cameraError) return
- if (!camera?.frigateHost) return null
+ if (!camera || !camera?.frigateHost) return null
+
+ recStore.openedCamera = camera
return (
diff --git a/src/widgets/camera.stat.table/FrigateCameraStateTable.tsx b/src/widgets/camera.stat.table/FrigateCameraStateTable.tsx
new file mode 100644
index 0000000..7bd4233
--- /dev/null
+++ b/src/widgets/camera.stat.table/FrigateCameraStateTable.tsx
@@ -0,0 +1,106 @@
+import { Table, Text } from '@mantine/core';
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { v4 as uuidv4 } from 'uuid';
+import SortedTh from '../../shared/components/table.aps/SortedTh';
+import { isProduction } from '../../shared/env.const';
+import { sortByKey } from '../../shared/utils/sort.array';
+
+
+export interface CameraItem {
+ cameraName: string,
+ process: ProcessType,
+ pid: number,
+ fps: number,
+ cpu: string,
+ mem: string,
+}
+
+export enum ProcessType {
+ Ffmpeg = "Ffmpeg",
+ Capture = "Capture",
+ Detect = "Detect",
+}
+
+interface TableHead {
+ propertyName: string,
+ title: string,
+ sorting?: boolean,
+}
+
+interface TableProps {
+ data: T[],
+}
+
+const FrigateCamerasStateTable = ({
+ data
+}: TableProps) => {
+
+ const { t } = useTranslation()
+
+ const [tableData, setTableData] = useState(data)
+ const [reversed, setReversed] = useState(false)
+ const [sortedName, setSortedName] = useState(null)
+
+
+ const handleSort = (headName: string, propertyName: string,) => {
+ const reverse = headName === sortedName ? !reversed : false;
+ setReversed(reverse)
+ const arr = sortByKey(tableData, propertyName as keyof CameraItem)
+ if (reverse) arr.reverse()
+ setTableData(arr)
+ setSortedName(headName)
+ }
+
+ const headTitle: TableHead[] = [
+ { propertyName: 'cameraName', title: t('camera') },
+ { propertyName: 'process', title: t('cameraStatTable.process') },
+ { propertyName: 'pid', title: t('cameraStatTable.pid'), sorting: false },
+ { propertyName: 'fps', title: t('cameraStatTable.fps') },
+ { propertyName: 'cpu', title: t('cameraStatTable.cpu') },
+ { propertyName: 'mem', title: t('cameraStatTable.memory') },
+ ]
+
+
+ const tableHead = headTitle.map(head => {
+ return (
+ handleSort(head.title, head.propertyName ? head.propertyName : '')}
+ sorting={head.sorting} />
+ )
+ })
+
+ if (!isProduction) console.log('FrigateHostsTable rendered')
+
+ const rows = tableData.map(item => {
+ return (
+
+ | {item.cameraName} |
+ {item.process} |
+ {item.pid} |
+ {item.fps} |
+ {item.cpu} |
+ {item.mem} |
+
+ )
+ })
+
+ return (
+
+
+
+ {tableHead}
+
+
+
+ {rows}
+
+
+ );
+};
+
+export default FrigateCamerasStateTable;
\ No newline at end of file
diff --git a/src/widgets/FrigateHostsTable.tsx b/src/widgets/hosts.table/FrigateHostsTable.tsx
similarity index 83%
rename from src/widgets/FrigateHostsTable.tsx
rename to src/widgets/hosts.table/FrigateHostsTable.tsx
index 82d802f..fa27353 100644
--- a/src/widgets/FrigateHostsTable.tsx
+++ b/src/widgets/hosts.table/FrigateHostsTable.tsx
@@ -3,14 +3,15 @@ import { IconPlus, IconTrash } from '@tabler/icons-react';
import ObjectId from 'bson-objectid';
import React, { useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
-import { GetFrigateHost } from '../services/frigate.proxy/frigate.schema';
-import HostSettingsMenu from '../shared/components/menu/HostSettingsMenu';
-import SortedTh from '../shared/components/table.aps/SortedTh';
-import { isProduction } from '../shared/env.const';
-import StateCell from './hosts.table/StateCell';
-import SwitchCell from './hosts.table/SwitchCell';
-import TextInputCell from './hosts.table/TextInputCell';
+import { GetFrigateHost } from '../../services/frigate.proxy/frigate.schema';
+import HostSettingsMenu from '../../shared/components/menu/HostSettingsMenu';
+import SortedTh from '../../shared/components/table.aps/SortedTh';
+import { isProduction } from '../../shared/env.const';
+import StateCell from './StateCell';
+import SwitchCell from './SwitchCell';
+import TextInputCell from './TextInputCell';
import { useTranslation } from 'react-i18next';
+import { sortByKey } from '../../shared/utils/sort.array';
interface TableProps {
data: T[],
@@ -37,20 +38,6 @@ const FrigateHostsTable = ({ data, showAddButton = false, saveCallback, changedC
changedCallback(tableData)
}, [tableData])
- function sortByKey(array: T[], key: K): T[] {
- return array.sort((a, b) => {
- let valueA = a[key];
- let valueB = b[key];
-
- const stringValueA = String(valueA).toLowerCase();
- const stringValueB = String(valueB).toLowerCase();
-
- if (stringValueA < stringValueB) return -1;
- if (stringValueA > stringValueB) return 1;
- return 0;
- });
- }
-
const handleSort = (headName: string, propertyName: string,) => {
const reverse = headName === sortedName ? !reversed : false;
setReversed(reverse)
diff --git a/src/widgets/hosts.table/StateCell.tsx b/src/widgets/hosts.table/StateCell.tsx
index a2c8381..8f7d5e0 100644
--- a/src/widgets/hosts.table/StateCell.tsx
+++ b/src/widgets/hosts.table/StateCell.tsx
@@ -1,4 +1,4 @@
-import { Flex } from '@mantine/core';
+import { Flex, Loader } from '@mantine/core';
import { IconPower } from '@tabler/icons-react';
import { useQuery } from '@tanstack/react-query';
import { frigateApi, frigateQueryKeys } from '../../services/frigate.proxy/frigate.api';
@@ -11,7 +11,7 @@ const StateCell = ({
id,
width,
}: StateCellProps) => {
- const { data } = useQuery({
+ const { data, isPending } = useQuery({
queryKey: [frigateQueryKeys.getFrigateHosts, id],
queryFn: frigateApi.getHosts,
staleTime: 60 * 1000,
@@ -23,9 +23,13 @@ const StateCell = ({
return (
-
-
-
+ {isPending ?
+
+ :
+
+
+
+ }
|
);
};