mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-05 21:47:48 +01:00
Compare commits
3 Commits
a6ab095ff9
...
44405b4aae
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44405b4aae | ||
|
|
842cec2fee | ||
|
|
c6ad937619 |
@@ -139,6 +139,7 @@ You can also find a list of all available service widgets in the sidebar navigat
|
|||||||
- [TubeArchivist](tubearchivist.md)
|
- [TubeArchivist](tubearchivist.md)
|
||||||
- [UniFi Controller](unifi-controller.md)
|
- [UniFi Controller](unifi-controller.md)
|
||||||
- [Unmanic](unmanic.md)
|
- [Unmanic](unmanic.md)
|
||||||
|
- [Unraid](unraid.md)
|
||||||
- [Uptime Kuma](uptime-kuma.md)
|
- [Uptime Kuma](uptime-kuma.md)
|
||||||
- [UptimeRobot](uptimerobot.md)
|
- [UptimeRobot](uptimerobot.md)
|
||||||
- [UrBackup](urbackup.md)
|
- [UrBackup](urbackup.md)
|
||||||
|
|||||||
28
docs/widgets/services/unraid.md
Normal file
28
docs/widgets/services/unraid.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: Unraid
|
||||||
|
description: Unraid Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Unraid](https://unraid.net/).
|
||||||
|
|
||||||
|
The Unraid widget allows you to monitor the resources of an Unraid server.
|
||||||
|
|
||||||
|
**Minimum Requirements:**
|
||||||
|
|
||||||
|
- Unraid 7.2 -or- Unraid Connect plugin 2025.08.19.1850
|
||||||
|
- API key with the **GUEST** (read only) role: [Managing API Keys](https://docs.unraid.net/go/managing-api-keys)
|
||||||
|
|
||||||
|
The widget can display metrics for selected Unraid pools. If using one of the "pool" fields, you must also add the pool name to the settings.
|
||||||
|
|
||||||
|
**Allowed fields:** `["cpu","memoryPercent","memoryAvailable","memoryUsed","notifications","arrayFreeSpace","arrayUsedSpace","arrayUsedPercent","status","pool1UsedSpace","pool1FreeSpace","pool1UsedPercent","pool2UsedSpace","pool2FreeSpace","pool2UsedPercent","pool3UsedSpace","pool3FreeSpace","pool3UsedPercent","pool4UsedSpace","pool4FreeSpace","pool4UsedPercent"]`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: unraid
|
||||||
|
url: https://unraid.host.or.ip
|
||||||
|
key: api-key
|
||||||
|
pool1: pool1name # required only if using pool1 fields
|
||||||
|
pool2: pool2name # required only if using pool2 fields
|
||||||
|
pool3: pool3name # required only if using pool3 fields
|
||||||
|
pool4: pool4name # required only if using pool4 fields
|
||||||
|
```
|
||||||
@@ -165,6 +165,7 @@ nav:
|
|||||||
- widgets/services/tubearchivist.md
|
- widgets/services/tubearchivist.md
|
||||||
- widgets/services/unifi-controller.md
|
- widgets/services/unifi-controller.md
|
||||||
- widgets/services/unmanic.md
|
- widgets/services/unmanic.md
|
||||||
|
- widgets/services/unraid.md
|
||||||
- widgets/services/uptime-kuma.md
|
- widgets/services/uptime-kuma.md
|
||||||
- widgets/services/uptimerobot.md
|
- widgets/services/uptimerobot.md
|
||||||
- widgets/services/urbackup.md
|
- widgets/services/urbackup.md
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "homepage",
|
"name": "homepage",
|
||||||
"version": "1.4.5",
|
"version": "1.4.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
|
|||||||
@@ -1083,5 +1083,27 @@
|
|||||||
"nextMonthlyCost": "Next Month",
|
"nextMonthlyCost": "Next Month",
|
||||||
"previousMonthlyCost": "Prev. Month",
|
"previousMonthlyCost": "Prev. Month",
|
||||||
"nextRenewingSubscription": "Next Payment"
|
"nextRenewingSubscription": "Next Payment"
|
||||||
|
},
|
||||||
|
"unraid": {
|
||||||
|
"STARTED": "Started",
|
||||||
|
"STOPPED": "Stopped",
|
||||||
|
"NEW_ARRAY": "New Array",
|
||||||
|
"RECON_DISK": "Reconstructing Disk",
|
||||||
|
"DISABLE_DISK": "Disk Disabled",
|
||||||
|
"SWAP_DSBL": "Swap Disable",
|
||||||
|
"INVALID_EXPANSION": "Invalid Expansion",
|
||||||
|
"PARITY_NOT_BIGGEST": "Parity Not Biggest",
|
||||||
|
"TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
|
||||||
|
"NEW_DISK_TOO_SMALL": "New Disk Too Small",
|
||||||
|
"NO_DATA_DISKS": "No Data Disks",
|
||||||
|
"notifications": "Notifications",
|
||||||
|
"status": "Status",
|
||||||
|
"cpu": "CPU",
|
||||||
|
"memoryUsed": "Memory Used",
|
||||||
|
"memoryAvailable": "Memory Available",
|
||||||
|
"arrayUsed": "Array Used",
|
||||||
|
"arrayFree": "Array Free",
|
||||||
|
"poolUsed": "{{pool}} Used",
|
||||||
|
"poolFree": "{{pool}} Free"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ export default function Container({ error = false, children, service }) {
|
|||||||
if (!field.includes(".")) {
|
if (!field.includes(".")) {
|
||||||
fullField = `${type}.${field}`;
|
fullField = `${type}.${field}`;
|
||||||
}
|
}
|
||||||
let matches = fullField === child?.props?.label;
|
let matches = fullField === (child?.props?.field || child?.props?.label);
|
||||||
// check if the field is an 'alias'
|
// check if the field is an 'alias'
|
||||||
if (matches) {
|
if (matches) {
|
||||||
return true;
|
return true;
|
||||||
} else if (ALIASED_WIDGETS[type]) {
|
} else if (ALIASED_WIDGETS[type]) {
|
||||||
matches = fullField.replace(type, ALIASED_WIDGETS[type]) === child?.props?.label;
|
matches = fullField.replace(type, ALIASED_WIDGETS[type]) === (child?.props?.field || child?.props?.label);
|
||||||
|
|
||||||
return matches;
|
return matches;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -396,6 +396,12 @@ export function cleanServiceGroups(groups) {
|
|||||||
// unifi
|
// unifi
|
||||||
site,
|
site,
|
||||||
|
|
||||||
|
// unraid
|
||||||
|
pool1,
|
||||||
|
pool2,
|
||||||
|
pool3,
|
||||||
|
pool4,
|
||||||
|
|
||||||
// vikunja
|
// vikunja
|
||||||
enableTaskList,
|
enableTaskList,
|
||||||
|
|
||||||
@@ -611,6 +617,12 @@ export function cleanServiceGroups(groups) {
|
|||||||
if (type === "grafana") {
|
if (type === "grafana") {
|
||||||
if (alerts) widget.alerts = alerts;
|
if (alerts) widget.alerts = alerts;
|
||||||
}
|
}
|
||||||
|
if (type === "unraid") {
|
||||||
|
if (pool1) widget.pool1 = pool1;
|
||||||
|
if (pool2) widget.pool2 = pool2;
|
||||||
|
if (pool3) widget.pool3 = pool3;
|
||||||
|
if (pool4) widget.pool4 = pool4;
|
||||||
|
}
|
||||||
return widget;
|
return widget;
|
||||||
});
|
});
|
||||||
return cleanedService;
|
return cleanedService;
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ const components = {
|
|||||||
truenas: dynamic(() => import("./truenas/component")),
|
truenas: dynamic(() => import("./truenas/component")),
|
||||||
unifi: dynamic(() => import("./unifi/component")),
|
unifi: dynamic(() => import("./unifi/component")),
|
||||||
unmanic: dynamic(() => import("./unmanic/component")),
|
unmanic: dynamic(() => import("./unmanic/component")),
|
||||||
|
unraid: dynamic(() => import("./unraid/component")),
|
||||||
uptimekuma: dynamic(() => import("./uptimekuma/component")),
|
uptimekuma: dynamic(() => import("./uptimekuma/component")),
|
||||||
uptimerobot: dynamic(() => import("./uptimerobot/component")),
|
uptimerobot: dynamic(() => import("./uptimerobot/component")),
|
||||||
urbackup: dynamic(() => import("./urbackup/component")),
|
urbackup: dynamic(() => import("./urbackup/component")),
|
||||||
|
|||||||
93
src/widgets/unraid/component.jsx
Normal file
93
src/widgets/unraid/component.jsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import Block from "components/services/widget/block";
|
||||||
|
import Container from "components/services/widget/container";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
|
const UNRAID_DEFAULT_FIELDS = ["status", "cpu", "memoryPercent", "notifications"];
|
||||||
|
const MAX_ALLOWED_FIELDS = 4;
|
||||||
|
|
||||||
|
const POOLS = ["pool1", "pool2", "pool3", "pool4"];
|
||||||
|
const POOL_FIELDS = [
|
||||||
|
{ param: "UsedSpace", label: "poolUsed", valueKey: "fsUsed", valueType: "common.bytes" },
|
||||||
|
{ param: "FreeSpace", label: "poolFree", valueKey: "fsFree", valueType: "common.bytes" },
|
||||||
|
{ param: "UsedPercent", label: "poolUsed", valueKey: "fsUsedPercent", valueType: "common.percent" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { widget } = service;
|
||||||
|
|
||||||
|
const { data, error } = useWidgetAPI(widget);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <Container service={service} error={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widget.fields?.length) {
|
||||||
|
widget.fields = UNRAID_DEFAULT_FIELDS;
|
||||||
|
} else if (widget.fields.length > MAX_ALLOWED_FIELDS) {
|
||||||
|
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="unraid.status" />
|
||||||
|
<Block label="unraid.memoryAvailable" />
|
||||||
|
<Block label="unraid.memoryUsed" />
|
||||||
|
<Block field="unraid.memoryPercent" label="unraid.memoryUsed" />
|
||||||
|
<Block label="unraid.cpu" />
|
||||||
|
<Block label="unraid.notifications" />
|
||||||
|
<Block field="unraid.arrayUsedSpace" label="unraid.arrayUsed" />
|
||||||
|
<Block field="unraid.arrayFree" label="unraid.arrayFree" />
|
||||||
|
<Block field="unraid.arrayUsedPercent" label="unraid.arrayUsed" />
|
||||||
|
{...POOLS.flatMap((pool) =>
|
||||||
|
POOL_FIELDS.map(({ param, label }) => (
|
||||||
|
<Block
|
||||||
|
key={`${pool}-${param}`}
|
||||||
|
field={`unraid.${pool}${param}`}
|
||||||
|
label={t(`unraid.${label}`, { pool: widget?.[pool] || pool })}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="unraid.status" value={t(`unraid.${data.arrayState}`)} />
|
||||||
|
<Block label="unraid.memoryAvailable" value={t("common.bbytes", { value: data.memoryAvailable })} />
|
||||||
|
<Block label="unraid.memoryUsed" value={t("common.bbytes", { value: data.memoryUsed })} />
|
||||||
|
<Block
|
||||||
|
field="unraid.memoryPercent"
|
||||||
|
label="unraid.memoryUsed"
|
||||||
|
value={t("common.percent", { value: data.memoryUsedPercent })}
|
||||||
|
/>
|
||||||
|
<Block label="unraid.cpu" value={t("common.percent", { value: data.cpuPercent })} />
|
||||||
|
<Block label="unraid.notifications" value={t("common.number", { value: data.unreadNotifications })} />
|
||||||
|
<Block
|
||||||
|
field="unraid.arrayUsedSpace"
|
||||||
|
label="unraid.arrayUsed"
|
||||||
|
value={t("common.bytes", { value: data.arrayUsed })}
|
||||||
|
/>
|
||||||
|
<Block label="unraid.arrayFree" value={t("common.bytes", { value: data.arrayFree })} />
|
||||||
|
<Block
|
||||||
|
field="unraid.arrayUsedPercent"
|
||||||
|
label="unraid.arrayUsed"
|
||||||
|
value={t("common.percent", { value: data.arrayUsedPercent })}
|
||||||
|
/>
|
||||||
|
{...POOLS.flatMap((pool) =>
|
||||||
|
POOL_FIELDS.map(({ param, label, valueKey, valueType }) => (
|
||||||
|
<Block
|
||||||
|
key={`${pool}-${param}`}
|
||||||
|
field={`unraid.${pool}${param}`}
|
||||||
|
label={t(`unraid.${label}`, { pool: widget?.[pool] || pool })}
|
||||||
|
value={t(valueType, { value: data.caches?.[widget?.[pool]]?.[valueKey] || "-" })}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
138
src/widgets/unraid/proxy.js
Normal file
138
src/widgets/unraid/proxy.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import getServiceWidget from "utils/config/service-helpers";
|
||||||
|
import createLogger from "utils/logger";
|
||||||
|
import { asJson } from "utils/proxy/api-helpers";
|
||||||
|
import { httpProxy } from "utils/proxy/http";
|
||||||
|
|
||||||
|
const logger = createLogger("unraidProxyHandler");
|
||||||
|
|
||||||
|
const graphqlQuery = `
|
||||||
|
{
|
||||||
|
array {
|
||||||
|
state
|
||||||
|
capacity {
|
||||||
|
kilobytes {
|
||||||
|
free
|
||||||
|
total
|
||||||
|
used
|
||||||
|
}
|
||||||
|
}
|
||||||
|
caches {
|
||||||
|
name
|
||||||
|
fsType
|
||||||
|
fsSize
|
||||||
|
fsFree
|
||||||
|
fsUsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metrics {
|
||||||
|
memory {
|
||||||
|
active
|
||||||
|
available
|
||||||
|
percentTotal
|
||||||
|
}
|
||||||
|
cpu {
|
||||||
|
percentTotal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notifications {
|
||||||
|
overview {
|
||||||
|
unread {
|
||||||
|
total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function processUnraidResponse(data) {
|
||||||
|
const response = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
data = asJson(data)?.data;
|
||||||
|
|
||||||
|
response["memoryUsedPercent"] = data?.metrics?.memory?.percentTotal ?? null;
|
||||||
|
response["memoryUsed"] = data?.metrics?.memory?.active ?? null;
|
||||||
|
response["memoryAvailable"] = data?.metrics?.memory?.available ?? null;
|
||||||
|
response["cpuPercent"] = data?.metrics?.cpu?.percentTotal ?? null;
|
||||||
|
response["unreadNotifications"] = data?.notifications?.overview?.unread?.total ?? null;
|
||||||
|
response["arrayState"] = data?.array?.state ?? null;
|
||||||
|
response["arrayFree"] = data?.array?.capacity?.kilobytes?.free * 1000 ?? null;
|
||||||
|
response["arrayUsed"] = data?.array?.capacity?.kilobytes?.used * 1000 ?? null;
|
||||||
|
response["arrayUsedPercent"] =
|
||||||
|
(data?.array?.capacity?.kilobytes?.used / data?.array?.capacity?.kilobytes?.total) * 100 ?? null;
|
||||||
|
|
||||||
|
response["caches"] = {};
|
||||||
|
if (data?.array?.caches) {
|
||||||
|
data.array.caches.forEach((cache) => {
|
||||||
|
if (cache.fsType) {
|
||||||
|
response.caches[cache.name] = {
|
||||||
|
fsFree: cache.fsFree * 1000,
|
||||||
|
fsUsed: cache.fsUsed * 1000,
|
||||||
|
fsUsedPercent: (cache.fsUsed / cache.fsSize) * 100 ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { error: error.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function unraidProxyHandler(req, res) {
|
||||||
|
const { group, service, index } = req.query;
|
||||||
|
|
||||||
|
if (!group || !service) {
|
||||||
|
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const widget = await getServiceWidget(group, service, index);
|
||||||
|
if (!widget) {
|
||||||
|
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(widget.url + "/graphql");
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: `application/json`,
|
||||||
|
"X-API-Key": `${widget.key}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
};
|
||||||
|
params.body = JSON.stringify({
|
||||||
|
query: graphqlQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [status, , data] = await httpProxy(url, params);
|
||||||
|
|
||||||
|
if (status === 204 || status === 304) {
|
||||||
|
return res.status(status).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
logger.error(
|
||||||
|
"Error getting data from Unraid for service '%s' in group '%s': %d. Data: %s",
|
||||||
|
service,
|
||||||
|
group,
|
||||||
|
status,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
return res.status(status).send({ error: { message: "Error calling Unraid API.", data } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = processUnraidResponse(data);
|
||||||
|
if (result.error) {
|
||||||
|
logger.error("Error processing Unraid data: %s", result.error);
|
||||||
|
return res.status(500).json({ error: result.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
return res.status(status).send(result);
|
||||||
|
}
|
||||||
7
src/widgets/unraid/widget.js
Normal file
7
src/widgets/unraid/widget.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import unraidProxyHandler from "./proxy";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
proxyHandler: unraidProxyHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
||||||
@@ -130,6 +130,7 @@ import truenas from "./truenas/widget";
|
|||||||
import tubearchivist from "./tubearchivist/widget";
|
import tubearchivist from "./tubearchivist/widget";
|
||||||
import unifi from "./unifi/widget";
|
import unifi from "./unifi/widget";
|
||||||
import unmanic from "./unmanic/widget";
|
import unmanic from "./unmanic/widget";
|
||||||
|
import unraid from "./unraid/widget";
|
||||||
import uptimekuma from "./uptimekuma/widget";
|
import uptimekuma from "./uptimekuma/widget";
|
||||||
import uptimerobot from "./uptimerobot/widget";
|
import uptimerobot from "./uptimerobot/widget";
|
||||||
import urbackup from "./urbackup/widget";
|
import urbackup from "./urbackup/widget";
|
||||||
@@ -278,6 +279,7 @@ const widgets = {
|
|||||||
unifi,
|
unifi,
|
||||||
unifi_console: unifi,
|
unifi_console: unifi,
|
||||||
unmanic,
|
unmanic,
|
||||||
|
unraid,
|
||||||
uptimekuma,
|
uptimekuma,
|
||||||
uptimerobot,
|
uptimerobot,
|
||||||
urbackup,
|
urbackup,
|
||||||
|
|||||||
Reference in New Issue
Block a user