diff --git a/interface/package.json b/interface/package.json index e18d8c614..ad06a1c6c 100644 --- a/interface/package.json +++ b/interface/package.json @@ -35,7 +35,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-icons": "^5.4.0", - "react-router": "^7.0.2", + "react-router": "^7.1.0", "react-toastify": "^11.0.2", "typesafe-i18n": "^5.26.2", "typescript": "^5.7.2" diff --git a/interface/src/app/main/CustomEntities.tsx b/interface/src/app/main/CustomEntities.tsx index bbc3f3938..73e109827 100644 --- a/interface/src/app/main/CustomEntities.tsx +++ b/interface/src/app/main/CustomEntities.tsx @@ -57,7 +57,7 @@ const CustomEntities = () => { if (!dialogOpen && !numChanges) { void fetchEntities(); } - }, 3000); + }); const { send: writeEntities } = useRequest( (data: Entities) => writeCustomEntities(data), diff --git a/interface/src/app/main/Dashboard.tsx b/interface/src/app/main/Dashboard.tsx index b1379893c..a8e2b957e 100644 --- a/interface/src/app/main/Dashboard.tsx +++ b/interface/src/app/main/Dashboard.tsx @@ -149,7 +149,7 @@ const Dashboard = () => { if (!deviceValueDialogOpen) { void fetchDashboard(); } - }, 3000); + }); useEffect(() => { showAll diff --git a/interface/src/app/main/Devices.tsx b/interface/src/app/main/Devices.tsx index 1fc3ada7a..450116568 100644 --- a/interface/src/app/main/Devices.tsx +++ b/interface/src/app/main/Devices.tsx @@ -419,7 +419,7 @@ const Devices = () => { if (!deviceValueDialogOpen) { selectedDevice ? void sendDeviceData(selectedDevice) : void sendCoreData(); } - }, 3000); + }); const deviceValueDialogSave = async (devicevalue: DeviceValue) => { const id = Number(device_select.state.id); diff --git a/interface/src/app/main/Sensors.tsx b/interface/src/app/main/Sensors.tsx index f063a3343..8332f42c5 100644 --- a/interface/src/app/main/Sensors.tsx +++ b/interface/src/app/main/Sensors.tsx @@ -90,7 +90,7 @@ const Sensors = () => { if (!temperatureDialogOpen && !analogDialogOpen) { void fetchSensorData(); } - }, 3000); + }); const common_theme = useTheme({ BaseRow: ` diff --git a/interface/src/app/status/APStatus.tsx b/interface/src/app/status/APStatus.tsx index d9c9f24db..3a5b7f588 100644 --- a/interface/src/app/status/APStatus.tsx +++ b/interface/src/app/status/APStatus.tsx @@ -14,11 +14,12 @@ import type { Theme } from '@mui/material'; import * as APApi from 'api/ap'; -import { useAutoRequest } from 'alova/client'; +import { useRequest } from 'alova/client'; import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; import type { APStatusType } from 'types'; import { APNetworkStatus } from 'types'; +import { useInterval } from 'utils'; export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => { switch (status) { @@ -34,11 +35,11 @@ export const apStatusHighlight = ({ status }: APStatusType, theme: Theme) => { }; const APStatus = () => { - const { - data, - send: loadData, - error - } = useAutoRequest(APApi.readAPStatus, { pollingTime: 3000 }); + const { data, send: loadData, error } = useRequest(APApi.readAPStatus); + + useInterval(() => { + void loadData(); + }); const { LL } = useI18nContext(); useLayoutTitle(LL.ACCESS_POINT(0)); diff --git a/interface/src/app/status/Activity.tsx b/interface/src/app/status/Activity.tsx index 6e1a78c50..7c6b826e7 100644 --- a/interface/src/app/status/Activity.tsx +++ b/interface/src/app/status/Activity.tsx @@ -8,20 +8,21 @@ import { Table } from '@table-library/react-table-library/table'; import { useTheme as tableTheme } from '@table-library/react-table-library/theme'; -import { useAutoRequest } from 'alova/client'; +import { useRequest } from 'alova/client'; import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; import type { Translation } from 'i18n/i18n-types'; +import { useInterval } from 'utils'; import { readActivity } from '../../api/app'; import type { Stat } from '../main/types'; const SystemActivity = () => { - const { - data, - send: loadData, - error - } = useAutoRequest(readActivity, { pollingTime: 3000 }); + const { data, send: loadData, error } = useRequest(readActivity); + + useInterval(() => { + void loadData(); + }); const { LL } = useI18nContext(); diff --git a/interface/src/app/status/HardwareStatus.tsx b/interface/src/app/status/HardwareStatus.tsx index 45384e3c1..27b31a9c8 100644 --- a/interface/src/app/status/HardwareStatus.tsx +++ b/interface/src/app/status/HardwareStatus.tsx @@ -17,9 +17,10 @@ import { import * as SystemApi from 'api/system'; -import { useAutoRequest } from 'alova/client'; +import { useRequest } from 'alova/client'; import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; +import { useInterval } from 'utils'; import BBQKeesIcon from './bbqkees.svg'; @@ -32,11 +33,11 @@ const HardwareStatus = () => { useLayoutTitle(LL.HARDWARE()); - const { - data, - send: loadData, - error - } = useAutoRequest(SystemApi.readSystemStatus, { pollingTime: 3000 }); + const { data, send: loadData, error } = useRequest(SystemApi.readSystemStatus); + + useInterval(() => { + void loadData(); + }); const content = () => { if (!data) { diff --git a/interface/src/app/status/MqttStatus.tsx b/interface/src/app/status/MqttStatus.tsx index 3ba7943fd..911c3c5d6 100644 --- a/interface/src/app/status/MqttStatus.tsx +++ b/interface/src/app/status/MqttStatus.tsx @@ -15,11 +15,12 @@ import type { Theme } from '@mui/material'; import * as MqttApi from 'api/mqtt'; -import { useAutoRequest } from 'alova/client'; +import { useRequest } from 'alova/client'; import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; import type { MqttStatusType } from 'types'; import { MqttDisconnectReason } from 'types'; +import { useInterval } from 'utils'; export const mqttStatusHighlight = ( { enabled, connected }: MqttStatusType, @@ -54,11 +55,11 @@ export const mqttQueueHighlight = ( }; const MqttStatus = () => { - const { - data, - send: loadData, - error - } = useAutoRequest(MqttApi.readMqttStatus, { pollingTime: 3000 }); + const { data, send: loadData, error } = useRequest(MqttApi.readMqttStatus); + + useInterval(() => { + void loadData(); + }); const { LL } = useI18nContext(); useLayoutTitle('MQTT'); diff --git a/interface/src/app/status/NTPStatus.tsx b/interface/src/app/status/NTPStatus.tsx index aefd81802..160deb8bf 100644 --- a/interface/src/app/status/NTPStatus.tsx +++ b/interface/src/app/status/NTPStatus.tsx @@ -28,19 +28,20 @@ import type { Theme } from '@mui/material'; import * as NTPApi from 'api/ntp'; import { dialogStyle } from 'CustomTheme'; -import { useAutoRequest, useRequest } from 'alova/client'; +import { useRequest } from 'alova/client'; import { ButtonRow, FormLoader, SectionContent, useLayoutTitle } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; import type { NTPStatusType, Time } from 'types'; import { NTPSyncStatus } from 'types'; +import { useInterval } from 'utils'; import { formatDateTime, formatLocalDateTime } from 'utils'; const NTPStatus = () => { - const { - data, - send: loadData, - error - } = useAutoRequest(NTPApi.readNTPStatus, { pollingTime: 3000 }); + const { data, send: loadData, error } = useRequest(NTPApi.readNTPStatus); + + useInterval(() => { + void loadData(); + }); const [localTime, setLocalTime] = useState(''); const [settingTime, setSettingTime] = useState(false); diff --git a/interface/src/app/status/NetworkStatus.tsx b/interface/src/app/status/NetworkStatus.tsx index 696e02155..7185a947b 100644 --- a/interface/src/app/status/NetworkStatus.tsx +++ b/interface/src/app/status/NetworkStatus.tsx @@ -18,11 +18,12 @@ import type { Theme } from '@mui/material'; import * as NetworkApi from 'api/network'; -import { useAutoRequest } from 'alova/client'; +import { useRequest } from 'alova/client'; import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import { useI18nContext } from 'i18n/i18n-react'; import type { NetworkStatusType } from 'types'; import { NetworkConnectionStatus } from 'types'; +import { useInterval } from 'utils'; const isConnected = ({ status }: NetworkStatusType) => status === NetworkConnectionStatus.WIFI_STATUS_CONNECTED || @@ -81,11 +82,11 @@ const IPs = (status: NetworkStatusType) => { }; const NetworkStatus = () => { - const { - data, - send: loadData, - error - } = useAutoRequest(NetworkApi.readNetworkStatus, { pollingTime: 3000 }); + const { data, send: loadData, error } = useRequest(NetworkApi.readNetworkStatus); + + useInterval(() => { + void loadData(); + }); const { LL } = useI18nContext(); useLayoutTitle(LL.NETWORK(1)); diff --git a/interface/src/app/status/RestartMonitor.tsx b/interface/src/app/status/RestartMonitor.tsx index d3a7cede8..7006776b0 100644 --- a/interface/src/app/status/RestartMonitor.tsx +++ b/interface/src/app/status/RestartMonitor.tsx @@ -11,9 +11,10 @@ import { import { readSystemStatus } from 'api/system'; import { dialogStyle } from 'CustomTheme'; -import { useAutoRequest } from 'alova/client'; +import { useRequest } from 'alova/client'; import MessageBox from 'components/MessageBox'; import { useI18nContext } from 'i18n/i18n-react'; +import { useInterval } from 'utils'; const RestartMonitor = () => { const [errorMessage, setErrorMessage] = useState(); @@ -22,8 +23,7 @@ const RestartMonitor = () => { let count = 0; - const { data } = useAutoRequest(readSystemStatus, { - pollingTime: 1000, + const { data, send } = useRequest(readSystemStatus, { force: true, initialData: { status: 'Getting ready...' }, async middleware(_, next) { @@ -42,6 +42,10 @@ const RestartMonitor = () => { setErrorMessage(error.message); }); + useInterval(() => { + void send(); + }, 1000); // check every second + return ( diff --git a/interface/src/app/status/Status.tsx b/interface/src/app/status/Status.tsx index 7d0785af9..a3ce5e18a 100644 --- a/interface/src/app/status/Status.tsx +++ b/interface/src/app/status/Status.tsx @@ -30,13 +30,15 @@ import { API } from 'api/app'; import { readSystemStatus } from 'api/system'; import { dialogStyle } from 'CustomTheme'; -import { useAutoRequest, useRequest } from 'alova/client'; +import { useRequest } from 'alova/client'; import { type APIcall, busConnectionStatus } from 'app/main/types'; import { FormLoader, SectionContent, useLayoutTitle } from 'components'; import ListMenuItem from 'components/layout/ListMenuItem'; import { AuthenticatedContext } from 'contexts/authentication'; import { useI18nContext } from 'i18n/i18n-react'; import { NTPSyncStatus, NetworkConnectionStatus } from 'types'; +import { useInterval } from 'utils'; +import { formatDateTime } from 'utils/time'; import RestartMonitor from './RestartMonitor'; @@ -58,9 +60,8 @@ const SystemStatus = () => { data, send: loadData, error - } = useAutoRequest(readSystemStatus, { + } = useRequest(readSystemStatus, { initialData: [], - pollingTime: 3000, async middleware(_, next) { if (!restarting) { await next(); @@ -68,6 +69,10 @@ const SystemStatus = () => { } }); + useInterval(() => { + void loadData(); + }); + const theme = useTheme(); const formatDurationSec = (duration_sec: number) => { @@ -134,7 +139,7 @@ const SystemStatus = () => { case NTPSyncStatus.NTP_INACTIVE: return LL.INACTIVE(0); case NTPSyncStatus.NTP_ACTIVE: - return LL.ACTIVE(); + return LL.ACTIVE() + ' (' + formatDateTime(data.ntp_time ?? '') + ')'; default: return LL.UNKNOWN(); } @@ -301,7 +306,7 @@ const SystemStatus = () => { icon={DeviceHubIcon} bgcolor={activeHighlight(data.mqtt_status)} label="MQTT" - text={data.mqtt_status ? LL.ACTIVE() : LL.INACTIVE(0)} + text={data.mqtt_status ? LL.CONNECTED(0) : LL.INACTIVE(0)} to="/status/mqtt" /> diff --git a/interface/src/types/system.ts b/interface/src/types/system.ts index 94a9c18df..8b0023881 100644 --- a/interface/src/types/system.ts +++ b/interface/src/types/system.ts @@ -11,6 +11,7 @@ export interface SystemStatus { num_sensors: number; num_analogs: number; ntp_status: number; + ntp_time?: string; mqtt_status: boolean; ap_status: boolean; network_status: NetworkConnectionStatus; diff --git a/interface/src/utils/useInterval.ts b/interface/src/utils/useInterval.ts index 9b3c89bf2..e319b4896 100644 --- a/interface/src/utils/useInterval.ts +++ b/interface/src/utils/useInterval.ts @@ -1,7 +1,9 @@ import { useEffect, useRef } from 'react'; +const DEFAULT_DELAY = 3000; + // adapted from https://www.joshwcomeau.com/snippets/react-hooks/use-interval/ -export const useInterval = (callback: () => void, delay: number) => { +export const useInterval = (callback: () => void, delay: number = DEFAULT_DELAY) => { const intervalRef = useRef(null); const savedCallback = useRef<() => void>(callback); @@ -11,14 +13,12 @@ export const useInterval = (callback: () => void, delay: number) => { useEffect(() => { const tick = () => savedCallback.current(); - if (typeof delay === 'number') { - intervalRef.current = window.setInterval(tick, delay); - return () => { - if (intervalRef.current !== null) { - window.clearInterval(intervalRef.current); - } - }; - } + intervalRef.current = window.setInterval(tick, delay); + return () => { + if (intervalRef.current !== null) { + window.clearInterval(intervalRef.current); + } + }; }, [delay]); return intervalRef; diff --git a/interface/yarn.lock b/interface/yarn.lock index 85f873e0f..02f6e1535 100644 --- a/interface/yarn.lock +++ b/interface/yarn.lock @@ -1607,7 +1607,7 @@ __metadata: react: "npm:^19.0.0" react-dom: "npm:^19.0.0" react-icons: "npm:^5.4.0" - react-router: "npm:^7.0.2" + react-router: "npm:^7.1.0" react-toastify: "npm:^11.0.2" rollup-plugin-visualizer: "npm:^5.12.0" terser: "npm:^5.37.0" @@ -5589,9 +5589,9 @@ __metadata: languageName: node linkType: hard -"react-router@npm:^7.0.2": - version: 7.0.2 - resolution: "react-router@npm:7.0.2" +"react-router@npm:^7.1.0": + version: 7.1.0 + resolution: "react-router@npm:7.1.0" dependencies: "@types/cookie": "npm:^0.6.0" cookie: "npm:^1.0.1" @@ -5603,7 +5603,7 @@ __metadata: peerDependenciesMeta: react-dom: optional: true - checksum: 10c0/f6c04939218a3d7f2b03b215c2299eab4dbb0dea4a16e0acfd8bf181ec69ff42d66abdba10a25cc3297c514f052a0d03bfb80431225eb763bb27e4e5b0b4a106 + checksum: 10c0/c4f4c76dc885cb2b351575052417f0a95ad454ae7cc27f42e5dcde993697b206eab9f5eb3f9a3acb1284331d41f8667bd4d10e246d463c3debd7d4de3edaa50b languageName: node linkType: hard diff --git a/mock-api/rest_server.ts b/mock-api/rest_server.ts index 3043baf95..d1027c329 100644 --- a/mock-api/rest_server.ts +++ b/mock-api/rest_server.ts @@ -518,6 +518,7 @@ let system_status = { num_analogs: 1, free_heap: 143, ntp_status: 2, + ntp_time: '2021-04-01T14:25:42Z', mqtt_status: true, ap_status: false, network_status: 3, // wifi connected