Adding support for RackNerd widget.

This commit is contained in:
Sparky 2024-12-04 20:59:18 -08:00
parent c58f59c105
commit 3cce7e4006
9 changed files with 222 additions and 0 deletions

View File

@ -108,6 +108,7 @@ You can also find a list of all available service widgets in the sidebar navigat
- [PyLoad](pyload.md)
- [qBittorrent](qbittorrent.md)
- [QNAP](qnap.md)
- [RackNerd](racknerd.md)
- [Radarr](radarr.md)
- [Readarr](readarr.md)
- [ROMM](romm.md)

View File

@ -0,0 +1,22 @@
---
title: RackNerd
description: RackNerd Widget Configuration
---
Learn more about [RackNerd](https://racknerd.com).
Use key & hash. Information about the key & hash can be found under the [VPS](https://nerdvm.racknerd.com) control panel in the API section.
Allowed fields: `["ipAddress", "hddtotal", "bandwidthfree", "bandwidthused"]`.
Note `"memoryusage"` is deprecated as v1 of their API result will be always be 0.
Note `"status"` is not fully implemented.
Note hard drive free/used/percentage isn't functioning in v1 of their API result.
```yaml
widget:
type: racknerd
url: https://nerdvm.racknerd.com
key: token
hash: token
```

View File

@ -131,6 +131,7 @@ nav:
- widgets/services/pyload.md
- widgets/services/qbittorrent.md
- widgets/services/qnap.md
- widgets/services/racknerd.md
- widgets/services/radarr.md
- widgets/services/readarr.md
- widgets/services/romm.md

View File

@ -716,6 +716,14 @@
"numfiles": "Files",
"numshares": "Shared Items"
},
"racknerd": {
"ipAddress": "IP Address",
"memoryusage": "Memory Usage",
"hddtotal": "Total Space",
"bandwidthtotal": "Bandwidth Total",
"bandwidthused": "Bandwidth Used",
"bandwidthfree": "Bandwidth Free"
},
"kopia": {
"status": "Status",
"size": "Size",

View File

@ -104,6 +104,7 @@ const components = {
pyload: dynamic(() => import("./pyload/component")),
qbittorrent: dynamic(() => import("./qbittorrent/component")),
qnap: dynamic(() => import("./qnap/component")),
racknerd: dynamic(() => import("./racknerd/component")),
radarr: dynamic(() => import("./radarr/component")),
readarr: dynamic(() => import("./readarr/component")),
romm: dynamic(() => import("./romm/component")),

View File

@ -0,0 +1,71 @@
import { useTranslation } from "next-i18next";
import { useMemo } from "react";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
export const racknerdDefaultFields = ["ipAddress", "hddtotal", "bandwidthusage"];
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const params = {
key: widget.key,
hash: widget.hash,
};
const { data: racknerdData, error: racknerdError } = useWidgetAPI(widget, "serverinfo", {...params, action: 'info'});
// Support for fields (harddriveusage, memoryusage, bandwidthusage)
const [showIpAddress, showMemoryUsage, showHardDriveUsage, showBandwidthUsed, showBandwidthFree] = useMemo(() => {
// Default values if fields is not set
if (!widget.fields) return [true, false, true, true, true];
const hasIpAddress = widget.fields?.includes("ipAddress") || false;
const hasMemoryUsage = widget.fields?.includes("memoryusage") || false;
const hasHardDriveUsage = widget.fields?.includes("hddtotal") || false;
const hasBandwidthUsed = widget.fields?.includes("bandwidthused") || false;
const hasBandwidthFree = widget.fields?.includes("bandwidthfree") || false;
return [hasIpAddress, hasMemoryUsage, hasHardDriveUsage, hasBandwidthUsed, hasBandwidthFree];
}, [widget.fields]);
if (racknerdError) {
return <Container service={service} error={racknerdError} />;
}
if (!racknerdData) {
return (
<Container service={service}>
{showIpAddress && <Block label="racknerd.ipAddress" />}
{showMemoryUsage && <Block label="racknerd.memoryusage" />}
{showHardDriveUsage && <Block label="racknerd.hddtotal" />}
{showBandwidthUsed && <Block label="racknerd.bandwidthused" />}
{showBandwidthFree && <Block label="racknerd.bandwidthfree" />}
</Container>
);
}
const { racknerd: racknerdInfo } = racknerdData;
return (
<Container service={service}>
{showIpAddress && (<Block
label="racknerd.ipAddress"
value={racknerdInfo.ipAddress}
/>)}
{showMemoryUsage && (<Block
label="racknerd.memoryusage"
value={t("common.bbytes", { value: racknerdInfo.system.memoryused, maximumFractionDigits: 1 })}
/>)}
{showHardDriveUsage && (<Block
label="racknerd.hddtotal"
value={t("common.bbytes", { value: racknerdInfo.system.hdd_total, maximumFractionDigits: 1 })}
/>)}
{showBandwidthUsed && (<Block
label="racknerd.bandwidthused"
value={t("common.bbytes", { value: racknerdInfo.system.bandwidth_used, maximumFractionDigits: 1 })}
/>)}
{showBandwidthFree && (<Block
label="racknerd.bandwidthfree"
value={t("common.bbytes", { value: racknerdInfo.system.bandwidth_free, maximumFractionDigits: 1 })}
/>)}
</Container>
);
}

View File

@ -0,0 +1,100 @@
import { xml2json } from "xml-js";
import { racknerdDefaultFields } from "./component";
import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
const logger = createLogger("racknerdProxyHandler");
async function requestEndpoint(apiBaseUrl, action, params) {
const request = {
method: "POST"
};
let qs = "";
Object.entries(params).forEach(([key, value]) => {
qs += `&${key}=${value}`;
});
const apiUrl = `${apiBaseUrl}?action=${action}${qs}`;
const [status, , data] = await httpProxy(apiUrl, request);
if (status !== 200) {
logger.debug(`HTTP ${status} performing XMLRequest for ${action}`, data);
throw new Error(`Failed fetching '${action}'`);
}
const response = {};
try {
const jsonData = JSON.parse(xml2json(`<root>${data}</root>`, {compact: true}));
const responseElements = jsonData?.root || {};
Object.entries(responseElements).forEach(([responseKey, responseValue]) => {
/* eslint no-underscore-dangle: ["error", { "allow": ["_text"] }] */
response[responseKey] = responseValue?._text || "";
});
} catch (e) {
logger.debug(`Failed parsing ${action} response:`, data);
throw new Error(`Failed parsing '${action}' response`);
}
return response;
}
export default async function racknerdProxyHandler(req, res) {
const { group, service, index } = req.query;
const serviceWidget = await getServiceWidget(group, service, index);
if (!serviceWidget) {
res.status(500).json({ error: { message: "Service widget not found" } });
return;
}
if (!serviceWidget.url) {
res.status(500).json({ error: { message: "Service widget url not configured" } });
return;
}
const serviceWidgetUrl = new URL(serviceWidget.url);
const apiBaseUrl = `${serviceWidgetUrl.protocol}//${serviceWidgetUrl.hostname}/api/client/command.php`;
if (!serviceWidget.fields?.length > 0) {
serviceWidget.fields = racknerdDefaultFields;
}
const requestStatus = ["status"].some((field) => serviceWidget.fields?.includes(field));
const requestInfo = ["bandwidthused", "memoryusage", "hddtotal", "ipAddress"].some((field) => serviceWidget.fields?.includes(field));
const params = {
bw: serviceWidget.fields?.includes('bandwidthused') || serviceWidget.fields?.includes('bandwidthfree'),
hdd: serviceWidget.fields?.includes('hddtotal'),
ipAddr: serviceWidget.fields?.includes('ipAddress'),
mem: serviceWidget.fields?.includes('memoryusage'),
key: serviceWidget.key,
hash: serviceWidget.hash,
};
await Promise.all([
requestStatus ? requestEndpoint(apiBaseUrl, "status", params) : null,
requestInfo ? requestEndpoint(apiBaseUrl, "info", params) : null,
])
.then(([statusResponse, infoResponse]) => {
const memoryItems = infoResponse.mem?.split(',');
const hddItems = infoResponse.hdd?.split(',');
const bandwidthItems = infoResponse.bw?.split(',');
res.status(200).json({
racknerd: {
ipAddress: infoResponse.ipaddress || undefined,
system: {
status: statusResponse ? statusResponse.statusmsg : undefined,
memoryused: memoryItems ? parseFloat(memoryItems[1], 10) : undefined,
hdd_total: hddItems ? parseFloat(hddItems[0], 10) : undefined,
bandwidth_total: bandwidthItems ? parseFloat(bandwidthItems[0], 10) : undefined,
bandwidth_used: bandwidthItems ? parseFloat(bandwidthItems[1], 10) : undefined,
bandwidth_free: bandwidthItems ? parseFloat(bandwidthItems[2], 10) : undefined,
mem_total: memoryItems ? parseFloat(memoryItems[0], 10) : undefined,
mem_free: memoryItems ? parseFloat(memoryItems[2], 10) : undefined,
mem_percent: memoryItems ? parseFloat(memoryItems[3], 10) : undefined,
}
}
});
})
.catch((error) => {
res.status(500).json({ error: { message: error.message } });
});
}

View File

@ -0,0 +1,16 @@
import racknerdProxyHandler from "./proxy";
const widget = {
api: "{url}/{endpoint}",
proxyHandler: racknerdProxyHandler,
mappings: {
serverinfo: {
endpoint: "api/client/command.php",
params: ["key", "hash"],
optionalParams: ["bw", "mem", "hdd", "ipaddr"],
},
},
};
export default widget;

View File

@ -96,6 +96,7 @@ import pterodactyl from "./pterodactyl/widget";
import pyload from "./pyload/widget";
import qbittorrent from "./qbittorrent/widget";
import qnap from "./qnap/widget";
import racknerd from "./racknerd/widget";
import radarr from "./radarr/widget";
import readarr from "./readarr/widget";
import rutorrent from "./rutorrent/widget";
@ -232,6 +233,7 @@ const widgets = {
pyload,
qbittorrent,
qnap,
racknerd,
radarr,
readarr,
romm,