From 6287ce0518023b4a53cf2c5333907226a4e1f441 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Fri, 23 Jan 2026 12:36:08 -0800
Subject: [PATCH] Enhancement: dockhand service widget
Update common.json
---
public/locales/en/common.json | 6 ++
src/utils/config/service-helpers.js | 6 ++
src/widgets/components.js | 1 +
src/widgets/dockhand/component.jsx | 62 +++++++++++++++++++
src/widgets/dockhand/proxy.js | 94 +++++++++++++++++++++++++++++
src/widgets/dockhand/widget.js | 14 +++++
src/widgets/widgets.js | 2 +
7 files changed, 185 insertions(+)
create mode 100644 src/widgets/dockhand/component.jsx
create mode 100644 src/widgets/dockhand/proxy.js
create mode 100644 src/widgets/dockhand/widget.js
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 7adcd4c12..8d6feaf5d 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -1136,5 +1136,11 @@
"songs": "Songs",
"time": "Time",
"artists": "Artists"
+ },
+ "dockhand": {
+ "running": "Running",
+ "stopped": "Stopped",
+ "cpu": "CPU",
+ "memory": "Memory"
}
}
diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js
index dc141cd91..d358f9d2f 100644
--- a/src/utils/config/service-helpers.js
+++ b/src/utils/config/service-helpers.js
@@ -298,6 +298,9 @@ export function cleanServiceGroups(groups) {
container,
server,
+ // dockhand
+ environment,
+
// emby, jellyfin
enableBlocks,
enableNowPlaying,
@@ -605,6 +608,9 @@ export function cleanServiceGroups(groups) {
if (showTime) widget.showTime = showTime;
if (timezone) widget.timezone = timezone;
}
+ if (type === "dockhand") {
+ if (environment) widget.environment = environment;
+ }
if (type === "hdhomerun") {
if (tuner !== undefined) widget.tuner = tuner;
}
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 30bafd50a..7cb304755 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -29,6 +29,7 @@ const components = {
diskstation: dynamic(() => import("./diskstation/component")),
downloadstation: dynamic(() => import("./downloadstation/component")),
docker: dynamic(() => import("./docker/component")),
+ dockhand: dynamic(() => import("./dockhand/component")),
kubernetes: dynamic(() => import("./kubernetes/component")),
emby: dynamic(() => import("./emby/component")),
esphome: dynamic(() => import("./esphome/component")),
diff --git a/src/widgets/dockhand/component.jsx b/src/widgets/dockhand/component.jsx
new file mode 100644
index 000000000..235130b31
--- /dev/null
+++ b/src/widgets/dockhand/component.jsx
@@ -0,0 +1,62 @@
+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";
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+ const { widget } = service;
+
+ const { data: stats, error: statsError } = useWidgetAPI(widget, "dashboard/stats");
+
+ if (statsError) {
+ return ;
+ }
+
+ if (!stats) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ let running;
+ let stopped;
+ let cpuPercent;
+ let memoryPercent;
+
+ if (widget?.environment) {
+ // Filter by environment if set
+ const environment = stats.find((env) => env.name === widget.environment);
+ if (environment) {
+ running = environment?.containers?.running;
+ stopped = environment?.containers?.stopped ?? (environment?.containers?.total ?? 0) - (running ?? 0);
+ cpuPercent = environment?.metrics?.cpuPercent;
+ memoryPercent = environment?.metrics?.memoryPercent;
+ }
+ } else {
+ // Aggregate across all environments
+ running = stats.reduce((sum, env) => sum + (env?.containers?.running ?? 0), 0);
+ const total = stats.reduce((sum, env) => sum + (env?.containers?.total ?? 0), 0);
+ stopped = total - running;
+ const totalCpu = stats.reduce((sum, env) => sum + (env?.metrics?.cpuPercent ?? 0), 0);
+ const totalMemory = stats.reduce((sum, env) => sum + (env?.metrics?.memoryPercent ?? 0), 0);
+ const envCount = stats.length;
+ cpuPercent = envCount > 0 ? totalCpu / envCount : 0;
+ memoryPercent = envCount > 0 ? totalMemory / envCount : 0;
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/dockhand/proxy.js b/src/widgets/dockhand/proxy.js
new file mode 100644
index 000000000..528e91002
--- /dev/null
+++ b/src/widgets/dockhand/proxy.js
@@ -0,0 +1,94 @@
+import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+import { formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
+import validateWidgetData from "utils/proxy/validate-widget-data";
+import widgets from "widgets/widgets";
+
+const logger = createLogger("dockhandProxyHandler");
+
+async function login(widget) {
+ if (!widget.username || !widget.password) return false;
+
+ const baseUrl = widget.url?.replace(/\/+$/, "");
+ if (!baseUrl) return false;
+
+ const loginUrl = new URL(`${baseUrl}/api/auth/login`);
+
+ const [status] = await httpProxy(loginUrl, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ username: widget.username, password: widget.password }),
+ });
+
+ return status === 200;
+}
+
+export default async function dockhandProxyHandler(req, res, map) {
+ const { group, service, endpoint, 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" });
+ }
+
+ if (!widgets?.[widget.type]?.api) {
+ return res.status(403).json({ error: "Service does not support API calls" });
+ }
+
+ const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
+
+ let [status, contentType, data] = await httpProxy(url, {
+ method: req.method,
+ });
+
+ // Attempt login and retrying once
+ if (status === 401) {
+ const loggedIn = await login(widget);
+ if (loggedIn) {
+ [status, contentType, data] = await httpProxy(url, {
+ method: req.method,
+ });
+ }
+ }
+
+ let resultData = data;
+
+ if (resultData?.error?.url) {
+ resultData.error.url = sanitizeErrorURL(url);
+ }
+
+ if (status === 204 || status === 304) {
+ return res.status(status).end();
+ }
+
+ if (status >= 400) {
+ logger.error("HTTP Error %d calling %s", status, url.toString());
+ return res.status(status).json({
+ error: {
+ message: "HTTP Error",
+ url: sanitizeErrorURL(url),
+ data: Buffer.isBuffer(resultData) ? Buffer.from(resultData).toString() : resultData,
+ },
+ });
+ }
+
+ if (status === 200) {
+ if (!validateWidgetData(widget, endpoint, resultData)) {
+ return res.status(500).json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } });
+ }
+ if (map) resultData = map(resultData);
+ }
+
+ if (contentType) res.setHeader("Content-Type", contentType);
+ return res.status(status).send(resultData);
+}
diff --git a/src/widgets/dockhand/widget.js b/src/widgets/dockhand/widget.js
new file mode 100644
index 000000000..c4279a12a
--- /dev/null
+++ b/src/widgets/dockhand/widget.js
@@ -0,0 +1,14 @@
+import dockhandProxyHandler from "./proxy";
+
+const widget = {
+ api: "{url}/api/{endpoint}",
+ proxyHandler: dockhandProxyHandler,
+
+ mappings: {
+ "dashboard/stats": {
+ endpoint: "dashboard/stats",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index f7947de78..eeeae0cc1 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -23,6 +23,7 @@ import customapi from "./customapi/widget";
import deluge from "./deluge/widget";
import develancacheui from "./develancacheui/widget";
import diskstation from "./diskstation/widget";
+import dockhand from "./dockhand/widget";
import downloadstation from "./downloadstation/widget";
import emby from "./emby/widget";
import esphome from "./esphome/widget";
@@ -171,6 +172,7 @@ const widgets = {
deluge,
develancacheui,
diskstation,
+ dockhand,
downloadstation,
emby,
esphome,