show badge if there is an update available, which is cached

This commit is contained in:
proddy
2026-04-27 18:12:05 +02:00
parent 6473c55317
commit 6e76bcc9af
11 changed files with 240 additions and 168 deletions

View File

@@ -1,10 +1,13 @@
import { useState } from 'react';
import { useContext, useState } from 'react';
import { toast } from 'react-toastify';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import BuildIcon from '@mui/icons-material/Build';
import CancelIcon from '@mui/icons-material/Cancel';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import ImportExportIcon from '@mui/icons-material/ImportExport';
import LockIcon from '@mui/icons-material/Lock';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import SettingsBackupRestoreIcon from '@mui/icons-material/SettingsBackupRestore';
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
@@ -28,15 +31,23 @@ import { useRequest } from 'alova/client';
import type { APIcall } from 'app/main/types';
import { SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem';
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import SystemMonitor from '../status/SystemMonitor';
const Settings = () => {
const { LL } = useI18nContext();
const { versions } = useContext(AuthenticatedContext);
useLayoutTitle(LL.SETTINGS(0));
const firmwareText = versions?.current?.version
? `v${versions.current.version}`
: '';
const upgradeAvailable = versions?.current?.upgradeable ?? false;
const [confirmFactoryReset, setConfirmFactoryReset] = useState(false);
const [confirmRestart, setConfirmRestart] = useState(false);
const [restarting, setRestarting] = useState<boolean>();
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
@@ -50,6 +61,16 @@ const Settings = () => {
});
};
const doRestart = async () => {
setConfirmRestart(false);
setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
};
const handleFactoryResetClose = () => {
setConfirmFactoryReset(false);
};
@@ -58,6 +79,14 @@ const Settings = () => {
setConfirmFactoryReset(true);
};
const handleRestartClose = () => {
setConfirmRestart(false);
};
const handleRestartClick = () => {
setConfirmRestart(true);
};
if (restarting) {
return <SystemMonitor />;
}
@@ -65,6 +94,15 @@ const Settings = () => {
return (
<SectionContent>
<List>
<ListMenuItem
icon={BuildIcon}
bgcolor="#72caf9"
label="EMS-ESP Firmware"
text={firmwareText}
to="/status/version"
badge={upgradeAvailable}
/>
<ListMenuItem
icon={TuneIcon}
bgcolor="#134ba2"
@@ -156,6 +194,29 @@ const Settings = () => {
</DialogActions>
</Dialog>
<Dialog sx={dialogStyle} open={confirmRestart} onClose={handleRestartClose}>
<DialogTitle>{LL.RESTART()}</DialogTitle>
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={handleRestartClose}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={doRestart}
color="error"
>
{LL.RESTART()}
</Button>
</DialogActions>
</Dialog>
<Divider />
<Box
@@ -164,9 +225,18 @@ const Settings = () => {
display: 'flex',
justifyContent: 'flex-end',
flexWrap: 'nowrap',
whiteSpace: 'nowrap'
whiteSpace: 'nowrap',
gap: 1
}}
>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={handleRestartClick}
color="error"
>
{LL.RESTART()}
</Button>
<Button
startIcon={<SettingsBackupRestoreIcon />}
variant="outlined"

View File

@@ -1,25 +1,16 @@
import { useContext, useState } from 'react';
import { toast } from 'react-toastify';
import { useContext } from 'react';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import BuildIcon from '@mui/icons-material/Build';
import CancelIcon from '@mui/icons-material/Cancel';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import DirectionsBusIcon from '@mui/icons-material/DirectionsBus';
import LogoDevIcon from '@mui/icons-material/LogoDev';
import MemoryIcon from '@mui/icons-material/Memory';
import MonitorHeartIcon from '@mui/icons-material/MonitorHeart';
import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
import RouterIcon from '@mui/icons-material/Router';
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import WifiIcon from '@mui/icons-material/Wifi';
import {
Avatar,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
List,
ListItem,
ListItemAvatar,
@@ -27,12 +18,10 @@ import {
useTheme
} from '@mui/material';
import { API } from 'api/app';
import { readSystemStatus } from 'api/system';
import { dialogStyle } from 'CustomTheme';
import { useRequest } from 'alova/client';
import { type APIcall, busConnectionStatus } from 'app/main/types';
import { busConnectionStatus } from 'app/main/types';
import { FormLoader, SectionContent, useLayoutTitle } from 'components';
import ListMenuItem from 'components/layout/ListMenuItem';
import { AuthenticatedContext } from 'contexts/authentication';
@@ -41,8 +30,6 @@ import { NTPSyncStatus, NetworkConnectionStatus, SystemStatusCodes } from 'types
import { useInterval } from 'utils';
import { formatDateTime } from 'utils/time';
import SystemMonitor from './SystemMonitor';
const formatNumber = (num: number) => new Intl.NumberFormat().format(num);
const formatDurationSec = (
@@ -71,24 +58,7 @@ const SystemStatus = () => {
const { me } = useContext(AuthenticatedContext);
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
const [restarting, setRestarting] = useState<boolean>();
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
const {
data,
send: loadData,
error
} = useRequest(readSystemStatus, {
async middleware(_, next) {
if (!restarting) {
await next();
}
}
});
const { data, send: loadData, error } = useRequest(readSystemStatus);
useInterval(() => {
void loadData();
@@ -217,22 +187,6 @@ const SystemStatus = () => {
const activeHighlight = (value: boolean) =>
value ? theme.palette.success.main : theme.palette.info.main;
const doRestart = async () => {
setConfirmRestart(false);
setRestarting(true);
await sendAPI({ device: 'system', cmd: 'restart', id: 0 }).catch(
(error: Error) => {
toast.error(error.message);
}
);
};
const handleCloseRestartDialog = () => setConfirmRestart(false);
if (restarting) {
return <SystemMonitor />;
}
if (!data || !LL) {
return (
<SectionContent>
@@ -244,14 +198,6 @@ const SystemStatus = () => {
return (
<SectionContent>
<List>
<ListMenuItem
icon={BuildIcon}
bgcolor="#72caf9"
label="EMS-ESP Firmware"
text={`v${data.emsesp_version || ''}`}
to="version"
/>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: '#c5572c', color: 'white' }}>
@@ -262,16 +208,6 @@ const SystemStatus = () => {
primary={LL.STATUS_OF(LL.SYSTEM(0))}
secondary={`${systemStatus} (${LL.UPTIME()}: ${formatDurationSec(data.uptime, LL)})`}
/>
{me.admin && (
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
color="error"
onClick={() => setConfirmRestart(true)}
>
{LL.RESTART()}
</Button>
)}
</ListItem>
<ListMenuItem
@@ -341,33 +277,6 @@ const SystemStatus = () => {
to="/status/log"
/>
</List>
<Dialog
sx={dialogStyle}
open={confirmRestart}
onClose={handleCloseRestartDialog}
>
<DialogTitle>{LL.RESTART()}</DialogTitle>
<DialogContent dividers>{LL.RESTART_CONFIRM()}</DialogContent>
<DialogActions>
<Button
startIcon={<CancelIcon />}
variant="outlined"
onClick={handleCloseRestartDialog}
color="secondary"
>
{LL.CANCEL()}
</Button>
<Button
startIcon={<PowerSettingsNewIcon />}
variant="outlined"
onClick={doRestart}
color="error"
>
{LL.RESTART()}
</Button>
</DialogActions>
</Dialog>
</SectionContent>
);
};

View File

@@ -1,4 +1,4 @@
import { memo, useContext, useState } from 'react';
import { memo, useContext, useMemo, useState } from 'react';
import { Link } from 'react-router';
import { toast } from 'react-toastify';
@@ -40,6 +40,7 @@ import {
import { AuthenticatedContext } from 'contexts/authentication';
import { useI18nContext } from 'i18n/i18n-react';
import type { TranslationFunctions } from 'i18n/i18n-types';
import type { VersionInfo } from 'types';
import { prettyDateTime } from 'utils/time';
// Constants moved outside component to avoid recreation
@@ -70,26 +71,6 @@ interface VersionData {
developer_mode: boolean;
}
interface VersionInfo {
version: string;
date: string;
}
interface RemoteVersionInfo extends VersionInfo {
upgradeable: boolean;
}
interface CurrentVersionInfo extends VersionInfo {
type: 'stable' | 'dev';
}
// Response payload from the `getVersions` action
interface VersionsResponse {
current: CurrentVersionInfo;
stable?: RemoteVersionInfo;
dev?: RemoteVersionInfo;
}
// Memoized components for better performance
const VersionInfoDialog = memo(
({
@@ -432,10 +413,7 @@ const getPlatform = (data: VersionData): string => {
const Version = () => {
const { LL, locale } = useI18nContext();
const { me } = useContext(AuthenticatedContext);
const [latestVersion, setLatestVersion] = useState<VersionInfo>();
const [latestDevVersion, setLatestDevVersion] = useState<VersionInfo>();
const { me, versions } = useContext(AuthenticatedContext);
const [restarting, setRestarting] = useState<boolean>(false);
const [openInstallDialog, setOpenInstallDialog] = useState<boolean>(false);
@@ -447,16 +425,30 @@ const Version = () => {
const [openInstallPartitionDialog, setOpenInstallPartitionDialog] =
useState<boolean>(false);
const [usingDevVersion, setUsingDevVersion] = useState<boolean>(false);
const [fetchDevVersion, setFetchDevVersion] = useState<boolean>(false);
const [devUpgradeAvailable, setDevUpgradeAvailable] = useState<boolean>(false);
const [stableUpgradeAvailable, setStableUpgradeAvailable] =
useState<boolean>(false);
const [internetLive, setInternetLive] = useState<boolean>(false);
const [downloadOnly, setDownloadOnly] = useState<boolean>(false);
const [showVersionInfo, setShowVersionInfo] = useState<number>(0); // 1 = stable, 2 = dev, 3 = partition
const [firmwareSize, setFirmwareSize] = useState<number>(0);
const latestVersion = useMemo<VersionInfo | undefined>(
() =>
versions?.stable
? { version: versions.stable.version, date: versions.stable.date }
: undefined,
[versions?.stable]
);
const latestDevVersion = useMemo<VersionInfo | undefined>(
() =>
versions?.dev
? { version: versions.dev.version, date: versions.dev.date }
: undefined,
[versions?.dev]
);
const usingDevVersion = versions?.current?.type === 'dev';
const stableUpgradeAvailable = versions?.stable?.upgradeable ?? false;
const devUpgradeAvailable = versions?.dev?.upgradeable ?? false;
const internetLive = Boolean(versions?.stable || versions?.dev);
const { send: sendSetPartition } = useRequest(
(partition: string) => callAction({ action: 'setPartition', param: partition }),
{ immediate: false }
@@ -480,32 +472,6 @@ const Version = () => {
{ immediate: false }
);
// fetch latest stable/dev versions via the device. The C++ code makes a call to emsesp.org/versions.json itself
// if the device has no internet, stable/dev are omitted and the internetLive flag is set to false
useRequest(() => callAction({ action: 'getVersions' }))
.onSuccess((event) => {
const versions = event.data as VersionsResponse;
setUsingDevVersion(versions.current?.type === 'dev');
if (versions.stable) {
setLatestVersion({
version: versions.stable.version,
date: versions.stable.date
});
setStableUpgradeAvailable(versions.stable.upgradeable);
}
if (versions.dev) {
setLatestDevVersion({
version: versions.dev.version,
date: versions.dev.date
});
setDevUpgradeAvailable(versions.dev.upgradeable);
}
setInternetLive(Boolean(versions.stable || versions.dev));
})
.onError(() => {
setInternetLive(false);
});
const { send: sendAPI } = useRequest((data: APIcall) => API(data), {
immediate: false
});
@@ -640,15 +606,33 @@ const Version = () => {
if (!me.admin) return null;
const isUpdateAvailable = choice === LL.UPDATE_AVAILABLE();
return (
<Button
sx={{ ml: 1 }}
variant="outlined"
color={choice === LL.UPDATE_AVAILABLE() ? 'success' : 'warning'}
color={isUpdateAvailable ? 'success' : 'warning'}
size="small"
onClick={() => showFirmwareDialog(showingDev)}
>
{choice}
{isUpdateAvailable && (
<Box
component="span"
aria-label="update available"
sx={{
display: 'inline-block',
width: 8,
height: 8,
ml: 1,
verticalAlign: 'middle',
borderRadius: '50%',
backgroundColor: '#ffeb3b',
boxShadow: '0 0 6px rgba(255, 235, 59, 0.8)'
}}
/>
)}
</Button>
);
};