diff --git a/package.json b/package.json index bbe3cbf0c..ee6e7af6d 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "minecraft-ping-js": "^1.0.2", "next": "^12.3.1", "next-i18next": "^12.0.1", - "node-os-utils": "^1.3.7", + "osx-temperature-sensor": "^1.0.8", "pretty-bytes": "^6.0.0", "raw-body": "^2.5.1", "react": "^18.2.0", @@ -32,6 +32,7 @@ "react-icons": "^4.4.0", "shvl": "^3.0.0", "swr": "^1.3.0", + "systeminformation": "^5.17.12", "tough-cookie": "^4.1.2", "winston": "^3.8.2", "xml-js": "^1.6.11" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a3ce3fcd8..fabcb86ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ specifiers: minecraft-ping-js: ^1.0.2 next: ^12.3.1 next-i18next: ^12.0.1 - node-os-utils: ^1.3.7 + osx-temperature-sensor: ^1.0.8 postcss: ^8.4.16 prettier: ^2.7.1 pretty-bytes: ^6.0.0 @@ -36,6 +36,7 @@ specifiers: react-icons: ^4.4.0 shvl: ^3.0.0 swr: ^1.3.0 + systeminformation: ^5.17.12 tailwind-scrollbar: ^2.0.1 tailwindcss: ^3.1.8 tough-cookie: ^4.1.2 @@ -57,7 +58,7 @@ dependencies: minecraft-ping-js: 1.0.2 next: 12.3.1_biqbaboplfbrettd7655fr4n2y next-i18next: 12.0.1_azq6kxkn3od7qdylwkyksrwopy - node-os-utils: 1.3.7 + osx-temperature-sensor: 1.0.8 pretty-bytes: 6.0.0 raw-body: 2.5.1 react: 18.2.0 @@ -66,6 +67,7 @@ dependencies: react-icons: 4.4.0_react@18.2.0 shvl: 3.0.0 swr: 1.3.0_react@18.2.0 + systeminformation: 5.17.12 tough-cookie: 4.1.2 winston: 3.8.2 xml-js: 1.6.11 @@ -2301,10 +2303,6 @@ packages: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: false - /node-os-utils/1.3.7: - resolution: {integrity: sha512-fvnX9tZbR7WfCG5BAy3yO/nCLyjVWD6MghEq0z5FDfN+ZXpLWNITBdbifxQkQ25ebr16G0N7eRWJisOcMEHG3Q==} - dev: false - /node-releases/2.0.6: resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==} dev: true @@ -2446,6 +2444,13 @@ packages: word-wrap: 1.2.3 dev: true + /osx-temperature-sensor/1.0.8: + resolution: {integrity: sha512-Gl3b+bn7+oDDnqPa+4v/cg3yg9lnE8ppS7ivL3opBZh4i7h99JNmkm6zWmo0m2a83UUJu+C9D7lGP0OS8IlehA==} + engines: {node: '>=4.0.0'} + os: [darwin] + requiresBuild: true + dev: false + /p-limit/3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -3063,6 +3068,13 @@ packages: react: 18.2.0 dev: false + /systeminformation/5.17.12: + resolution: {integrity: sha512-I3pfMW2vue53u+X08BNxaJieaHkRoMMKjWetY9lbYJeWFaeWPO6P4FkNc4XOCX8F9vbQ0HqQ25RJoz3U/B7liw==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + dev: false + /tailwind-scrollbar/2.0.1_tailwindcss@3.1.8: resolution: {integrity: sha512-OcR7qHBbux4k+k6bWqnEQFYFooLK/F4dhkBz6nvswIoaA9ancZ5h20e0tyV7ifSWLDCUBtpG+1NHRA8HMRH/wg==} engines: {node: '>=12.13.0'} diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 92f429264..3eb3c4786 100755 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -36,7 +36,14 @@ "total": "Total", "free": "Free", "used": "Used", - "load": "Load" + "load": "Load", + "temp": "TEMP", + "max": "Max", + "uptime": "UP", + "months": "mo", + "days": "d", + "hours": "h", + "minutes": "m" }, "unifi": { "users": "Users", @@ -298,7 +305,11 @@ "glances": { "cpu": "CPU", "mem": "MEM", - "wait": "Please wait" + "wait": "Please wait", + "temp": "TEMP", + "uptime": "UP", + "days": "d", + "hours": "h" }, "quicklaunch": { "bookmark": "Bookmark", diff --git a/src/components/widgets/glances/glances.jsx b/src/components/widgets/glances/glances.jsx index a48cef50c..d91cb2633 100644 --- a/src/components/widgets/glances/glances.jsx +++ b/src/components/widgets/glances/glances.jsx @@ -1,6 +1,6 @@ import useSWR from "swr"; import { BiError } from "react-icons/bi"; -import { FaMemory } from "react-icons/fa"; +import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa"; import { FiCpu } from "react-icons/fi"; import { useTranslation } from "next-i18next"; @@ -64,6 +64,9 @@ export default function Widget({ options }) { ); } + const unit = options.units === "imperial" ? "fahrenheit" : "celsius"; + const mainTemp = (options.cputemp && data.sensors && unit === "celsius") ? data.sensors.find(s => s.label.includes("cpu_thermal")).value : data.sensors.find(s => s.label.includes("cpu_thermal")).value * 5/9 + 32; + return (
@@ -73,7 +76,7 @@ export default function Widget({ options }) {
{t("common.number", { - value: data.cpu, + value: data.quicklook.cpu, style: "unit", unit: "percent", maximumFractionDigits: 0, @@ -81,7 +84,7 @@ export default function Widget({ options }) {
{t("glances.cpu")}
- +
@@ -90,7 +93,7 @@ export default function Widget({ options }) {
{t("common.number", { - value: data.mem, + value: data.quicklook.mem, style: "unit", unit: "percent", maximumFractionDigits: 0, @@ -98,9 +101,38 @@ export default function Widget({ options }) {
{t("glances.mem")}
- +
+ {options.cputemp && + (
+ +
+ +
+ {t("common.number", { + value: mainTemp, + maximumFractionDigits: 1, + style: "unit", + unit + })} +
+
{t("glances.temp")}
+
+
+
)} + {options.uptime && data.uptime && + (
+ +
+ +
+ {data.uptime.replace(" days,", t("glances.days")).replace(/:\d\d:\d\d$/g, t("glances.hours"))} +
+
{t("glances.uptime")}
+
+
+
)} {options.label && (
{options.label}
diff --git a/src/components/widgets/resources/cputemp.jsx b/src/components/widgets/resources/cputemp.jsx new file mode 100644 index 000000000..d5cb61009 --- /dev/null +++ b/src/components/widgets/resources/cputemp.jsx @@ -0,0 +1,79 @@ +import useSWR from "swr"; +import { FaThermometerHalf } from "react-icons/fa"; +import { BiError } from "react-icons/bi"; +import { useTranslation } from "next-i18next"; + +export default function CpuTemp({ expanded, units }) { + const { t } = useTranslation(); + + const { data, error } = useSWR(`/api/widgets/resources?type=cputemp`, { + refreshInterval: 1500, + }); + + if (error || data?.error) { + return ( +
+ +
+ {t("widget.api_error")} +
+
+ ); + } + + if (!data) { + return ( +
+ +
+ +
-
+
{t("resources.temp")}
+
+ {expanded && ( + +
-
+
{t("resources.max")}
+
+ )} +
+
+ ); + } + + const unit = units === "imperial" ? "fahrenheit" : "celsius"; + const mainTemp = (unit === "celsius") ? data.cputemp.main : data.cputemp.main * 5/9 + 32; + const maxTemp = (unit === "celsius") ? data.cputemp.max : data.cputemp.max * 5/9 + 32; + + return ( +
+ +
+ +
+ {t("common.number", { + value: mainTemp, + maximumFractionDigits: 1, + style: "unit", + unit + })} +
+
{t("resources.temp")}
+
+ {expanded && ( + +
+ {t("common.number", { + value: maxTemp, + maximumFractionDigits: 1, + style: "unit", + unit + })} +
+
{t("resources.max")}
+
+ )} +
+
+ ); +} diff --git a/src/components/widgets/resources/disk.jsx b/src/components/widgets/resources/disk.jsx index fb770dbb1..dbc41b1cf 100644 --- a/src/components/widgets/resources/disk.jsx +++ b/src/components/widgets/resources/disk.jsx @@ -44,19 +44,19 @@ export default function Disk({ options, expanded }) { ); } - const percent = Math.round((data.drive.usedGb / data.drive.totalGb) * 100); + const percent = Math.round((data.drive.used / data.drive.size) * 100); return (
-
{t("common.bytes", { value: data.drive.freeGb * 1024 * 1024 * 1024 })}
+
{t("common.bytes", { value: data.drive.available })}
{t("resources.free")}
{expanded && ( -
{t("common.bytes", { value: data.drive.totalGb * 1024 * 1024 * 1024 })}
+
{t("common.bytes", { value: data.drive.size })}
{t("resources.total")}
)} diff --git a/src/components/widgets/resources/memory.jsx b/src/components/widgets/resources/memory.jsx index 27351998e..2ee0a15e6 100644 --- a/src/components/widgets/resources/memory.jsx +++ b/src/components/widgets/resources/memory.jsx @@ -44,7 +44,7 @@ export default function Memory({ expanded }) { ); } - const percent = Math.round((data.memory.usedMemMb / data.memory.totalMemMb) * 100); + const percent = Math.round((data.memory.used / data.memory.total) * 100); return (
@@ -52,7 +52,7 @@ export default function Memory({ expanded }) {
- {t("common.bytes", { value: data.memory.freeMemMb * 1024 * 1024, maximumFractionDigits: 1, binary: true })} + {t("common.bytes", { value: data.memory.free, maximumFractionDigits: 1, binary: true })}
{t("resources.free")}
@@ -60,7 +60,7 @@ export default function Memory({ expanded }) {
{t("common.bytes", { - value: data.memory.totalMemMb * 1024 * 1024, + value: data.memory.total, maximumFractionDigits: 1, binary: true, })} diff --git a/src/components/widgets/resources/resources.jsx b/src/components/widgets/resources/resources.jsx index 0524e39af..4ff0c81cd 100644 --- a/src/components/widgets/resources/resources.jsx +++ b/src/components/widgets/resources/resources.jsx @@ -1,9 +1,11 @@ import Disk from "./disk"; import Cpu from "./cpu"; import Memory from "./memory"; +import CpuTemp from "./cputemp"; +import Uptime from "./uptime"; export default function Resources({ options }) { - const { expanded } = options; + const { expanded, units } = options; return (
@@ -12,6 +14,8 @@ export default function Resources({ options }) { {Array.isArray(options.disk) ? options.disk.map((disk) => ) : options.disk && } + {options.cputemp && } + {options.uptime && }
{options.label && (
{options.label}
diff --git a/src/components/widgets/resources/uptime.jsx b/src/components/widgets/resources/uptime.jsx new file mode 100644 index 000000000..2624bef12 --- /dev/null +++ b/src/components/widgets/resources/uptime.jsx @@ -0,0 +1,61 @@ +import useSWR from "swr"; +import { FaRegClock } from "react-icons/fa"; +import { BiError } from "react-icons/bi"; +import { useTranslation } from "next-i18next"; + +export default function Uptime() { + const { t } = useTranslation(); + + const { data, error } = useSWR(`/api/widgets/resources?type=uptime`, { + refreshInterval: 1500, + }); + + if (error || data?.error) { + return ( +
+ +
+ {t("widget.api_error")} +
+
+ ); + } + + if (!data) { + return ( +
+ +
+ +
-
+
{t("resources.temp")}
+
+
+
+ ); + } + + const mo = Math.floor(data.uptime / (3600 * 24 * 31)); + const d = Math.floor(data.uptime % (3600 * 24 * 31) / (3600 * 24)); + const h = Math.floor(data.uptime % (3600 * 24) / 3600); + const m = Math.floor(data.uptime % 3600 / 60); + + let uptime; + if (mo > 0) uptime = `${mo}${t("resources.months")} ${d}${t("resources.days")}`; + else if (d > 0) uptime = `${d}${t("resources.days")} ${h}${t("resources.hours")}`; + else uptime = `${h}${t("resources.hours")} ${m}${t("resources.minutes")}`; + + return ( +
+ +
+ +
+ {uptime} +
+
{t("resources.uptime")}
+
+
+
+ ); +} diff --git a/src/pages/api/widgets/glances.js b/src/pages/api/widgets/glances.js index 5d4622b8d..46be14a05 100644 --- a/src/pages/api/widgets/glances.js +++ b/src/pages/api/widgets/glances.js @@ -4,19 +4,16 @@ import { getPrivateWidgetOptions } from "utils/config/widget-helpers"; const logger = createLogger("glances"); -export default async function handler(req, res) { - const { index } = req.query; - - const privateWidgetOptions = await getPrivateWidgetOptions("glances", index); - +async function retrieveFromGlancesAPI(privateWidgetOptions, endpoint) { + let errorMessage; const url = privateWidgetOptions?.url; if (!url) { - const errorMessage = "Missing Glances URL"; + errorMessage = "Missing Glances URL"; logger.error(errorMessage); - return res.status(400).json({ error: errorMessage }); + throw new Error(errorMessage); } - const apiUrl = `${url}/api/3/quicklook`; + const apiUrl = `${url}/api/3/${endpoint}`; const headers = { "Accept-Encoding": "application/json" }; @@ -25,16 +22,41 @@ export default async function handler(req, res) { } const params = { method: "GET", headers }; - const [status, contentType, data] = await httpProxy(apiUrl, params); + const [status, , data] = await httpProxy(apiUrl, params); if (status === 401) { - logger.error("Authorization failure getting data from glances API. Data: %s", data); + errorMessage = `Authorization failure getting data from glances API. Data: ${data.toString()}` + logger.error(errorMessage); + throw new Error(errorMessage); } - + if (status !== 200) { - logger.error("HTTP %d getting data from glances API. Data: %s", status, data); + errorMessage = `HTTP ${status} getting data from glances API. Data: ${data.toString()}` + logger.error(errorMessage); + throw new Error(errorMessage); } - if (contentType) res.setHeader("Content-Type", contentType); - return res.status(status).send(data); + return JSON.parse(Buffer.from(data).toString()); +} + +export default async function handler(req, res) { + const { index } = req.query; + + const privateWidgetOptions = await getPrivateWidgetOptions("glances", index); + + try { + const quicklookData = await retrieveFromGlancesAPI(privateWidgetOptions, "quicklook"); + + const data = { + quicklook: quicklookData + } + + data.uptime = await retrieveFromGlancesAPI(privateWidgetOptions, "uptime"); + + data.sensors = await retrieveFromGlancesAPI(privateWidgetOptions, "sensors"); + + return res.status(200).send(data); + } catch (e) { + return res.status(400).json({ error: e.message }); + } } diff --git a/src/pages/api/widgets/resources.js b/src/pages/api/widgets/resources.js index baa507a4e..ae53bf146 100644 --- a/src/pages/api/widgets/resources.js +++ b/src/pages/api/widgets/resources.js @@ -1,15 +1,16 @@ import { existsSync } from "fs"; -import { cpu, drive, mem } from "node-os-utils"; +const si = require('systeminformation'); export default async function handler(req, res) { const { type, target } = req.query; if (type === "cpu") { + const load = await si.currentLoad(); return res.status(200).json({ cpu: { - usage: await cpu.usage(1000), - load: cpu.loadavgTime(5), + usage: load.currentLoad, + load: load.avgLoad, }, }); } @@ -21,14 +22,29 @@ export default async function handler(req, res) { }); } + const fsSize = await si.fsSize(); + return res.status(200).json({ - drive: await drive.info(target || "/"), + drive: fsSize.find(fs => fs.mount === target) ?? fsSize.find(fs => fs.mount === "/") }); } if (type === "memory") { return res.status(200).json({ - memory: await mem.info(), + memory: await si.mem(), + }); + } + + if (type === "cputemp") { + return res.status(200).json({ + cputemp: await si.cpuTemperature(), + }); + } + + if (type === "uptime") { + const timeData = await si.time(); + return res.status(200).json({ + uptime: timeData.uptime }); }