From d6dde5fc4178d5d25179122b70ee20a30c5d6e23 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Mon, 19 Jan 2026 07:46:11 -0800
Subject: [PATCH 01/19] Documentaiton: clarify backend port usage in NetAlertX
widget docs
---
docs/widgets/services/netalertx.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/widgets/services/netalertx.md b/docs/widgets/services/netalertx.md
index a67de624c..5c28deeca 100644
--- a/docs/widgets/services/netalertx.md
+++ b/docs/widgets/services/netalertx.md
@@ -19,7 +19,7 @@ Provide the `API_TOKEN` (f.k.a. `SYNC_api_token`) as the `key` in your config.
```yaml
widget:
type: netalertx
- url: http://ip:port
+ url: http://ip:port # backend port
key: yournetalertxapitoken
version: 2 # optional, default is 1
```
From f524531a13d3aef7b319366476601399bd931c34 Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Mon, 19 Jan 2026 07:49:46 -0800
Subject: [PATCH 02/19] Fix truenas proxy widget logging
---
src/widgets/truenas/proxy.js | 14 +++-----------
1 file changed, 3 insertions(+), 11 deletions(-)
diff --git a/src/widgets/truenas/proxy.js b/src/widgets/truenas/proxy.js
index ebc5299ef..334b373bc 100644
--- a/src/widgets/truenas/proxy.js
+++ b/src/widgets/truenas/proxy.js
@@ -25,9 +25,6 @@ function waitForEvent(ws, handler, { event = "message", parseJson = true } = {})
} else if (typeof payload === "string") {
parsed = JSON.parse(payload);
}
- logger.info("Received TrueNAS websocket message: %o", parsed);
- } else {
- logger.info("Received TrueNAS websocket message: %o", payload);
}
const handlerResult = handler(parsed);
if (handlerResult !== undefined) {
@@ -52,7 +49,7 @@ function waitForEvent(ws, handler, { event = "message", parseJson = true } = {})
const handleClose = () => {
cleanup();
- logger.error("TrueNAS websocket connection closed unexpectedly");
+ logger.debug("TrueNAS websocket connection closed unexpectedly");
reject(new Error("TrueNAS websocket closed the connection"));
};
@@ -73,7 +70,6 @@ let nextId = 1;
async function sendMethod(ws, method, params = []) {
const id = nextId++;
const payload = { jsonrpc: "2.0", id, method, params };
- logger.info("Sending TrueNAS websocket method %s with id %d", method, id);
ws.send(JSON.stringify(payload));
return waitForEvent(ws, (message) => {
@@ -92,7 +88,7 @@ async function authenticate(ws, widget) {
if (apiKeyResult === true) return;
logger.warn("TrueNAS API key authentication failed, falling back to username/password when available.");
} catch (err) {
- logger.warn("TrueNAS API key authentication failed: %s", err?.message ?? err);
+ logger.error("TrueNAS API key authentication failed: %s", err?.message ?? err);
}
}
@@ -141,13 +137,9 @@ export default async function truenasProxyHandler(req, res, map) {
let data;
const wsUrl = new URL(formatApiCall(widgets[widget.type].wsAPI, { ...widget }));
const useSecure = wsUrl.protocol === "https:" || Boolean(widget.key); // API key requires secure connection
- if (useSecure && wsUrl.protocol !== "https:")
- logger.info("Upgrading TrueNAS websocket connection to secure wss://");
wsUrl.protocol = useSecure ? "wss:" : "ws:";
- logger.info("Connecting to TrueNAS websocket at %s", wsUrl);
const ws = new WebSocket(wsUrl, { rejectUnauthorized: false });
await waitForEvent(ws, () => true, { event: "open", parseJson: false }); // wait for open
- logger.info("Connected to TrueNAS websocket at %s", wsUrl);
try {
await authenticate(ws, widget);
data = await sendMethod(ws, wsMethod);
@@ -166,7 +158,7 @@ export default async function truenasProxyHandler(req, res, map) {
if (err?.status) {
return res.status(err.status).json({ error: err.message });
}
- logger.warn("Websocket call for TrueNAS failed: %s", err?.message ?? err);
+ logger.error("Websocket call for TrueNAS failed: %s", err?.message ?? err);
return res.status(500).json({ error: err?.message ?? "TrueNAS websocket call failed" });
}
}
From 6b6090e303f2880100520a5d8fe7d0b28975769b Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Tue, 20 Jan 2026 16:43:01 -0800
Subject: [PATCH 03/19] Documentation: use blurred image for bkgd instead of
filter
---
docs/stylesheets/extra.css | 16 +---------------
1 file changed, 1 insertion(+), 15 deletions(-)
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
index 7f299d2ec..43d9bb0a4 100644
--- a/docs/stylesheets/extra.css
+++ b/docs/stylesheets/extra.css
@@ -104,7 +104,7 @@
body {
background-color: transparent !important;
- background-image: url("https://raw.githubusercontent.com/gethomepage/homepage/main/docs/assets/blossom_valley.jpg");
+ background-image: url("https://raw.githubusercontent.com/gethomepage/homepage/main/docs/assets/blossom_valley_blur.jpg");
background-size: cover;
background-attachment: fixed;
background-position: center;
@@ -119,20 +119,6 @@ body[data-md-color-scheme="default"] {
color: rgba(255, 255, 255, 1);
}
-.blur-overlay {
- z-index: -1;
- position: fixed;
- width: 100%;
- height: 100%;
- background: hsl(0deg 0% 0% / 10%);
- backdrop-filter: blur(128px);
- -webkit-backdrop-filter: blur(128px);
-}
-
-[data-md-color-scheme="default"] .blur-overlay {
- background: hsla(0, 0%, 0%, 0);
-}
-
.md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link,
.md-nav--secondary .md-nav__title {
background: none;
From 6c945d657377be163550abc72a9d3432a1255cae Mon Sep 17 00:00:00 2001
From: shamoon <4887959+shamoon@users.noreply.github.com>
Date: Fri, 23 Jan 2026 13:20:56 -0800
Subject: [PATCH 04/19] Feature: dockhand service widget (#6229)
---
docs/widgets/services/dockhand.md | 20 +++++
docs/widgets/services/index.md | 1 +
mkdocs.yml | 1 +
public/locales/en/common.json | 14 ++++
src/utils/config/service-helpers.js | 6 ++
src/widgets/components.js | 1 +
src/widgets/dockhand/component.jsx | 123 ++++++++++++++++++++++++++++
src/widgets/dockhand/proxy.js | 70 ++++++++++++++++
src/widgets/dockhand/widget.js | 14 ++++
src/widgets/widgets.js | 2 +
10 files changed, 252 insertions(+)
create mode 100644 docs/widgets/services/dockhand.md
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/docs/widgets/services/dockhand.md b/docs/widgets/services/dockhand.md
new file mode 100644
index 000000000..7267d43bd
--- /dev/null
+++ b/docs/widgets/services/dockhand.md
@@ -0,0 +1,20 @@
+---
+title: Dockhand
+description: Dockhand Widget Configuration
+---
+
+Learn more about [Dockhand](https://dockhand.pro/).
+
+Note: The widget currently supports Dockhand's **local** authentication only.
+
+**Allowed fields:** (max 4): `running`, `stopped`, `paused`, `total`, `cpu`, `memory`, `images`, `volumes`, `events_today`, `pending_updates`, `stacks`.
+**Default fields:** `running`, `total`, `cpu`, `memory`.
+
+```yaml
+widget:
+ type: dockhand
+ url: http://localhost:3001
+ environment: local # optional: name or id; aggregates all when omitted
+ username: your-user # required for local auth
+ password: your-pass # required for local auth
+```
diff --git a/docs/widgets/services/index.md b/docs/widgets/services/index.md
index 8110a7fc4..190dddec7 100644
--- a/docs/widgets/services/index.md
+++ b/docs/widgets/services/index.md
@@ -32,6 +32,7 @@ You can also find a list of all available service widgets in the sidebar navigat
- [Deluge](deluge.md)
- [DeveLanCacheUI](develancacheui.md)
- [DiskStation](diskstation.md)
+- [Dockhand](dockhand.md)
- [DownloadStation](downloadstation.md)
- [Emby](emby.md)
- [ESPHome](esphome.md)
diff --git a/mkdocs.yml b/mkdocs.yml
index 2a21cbfc6..a7bc0d87b 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -56,6 +56,7 @@ nav:
- widgets/services/deluge.md
- widgets/services/develancacheui.md
- widgets/services/diskstation.md
+ - widgets/services/dockhand.md
- widgets/services/downloadstation.md
- widgets/services/emby.md
- widgets/services/esphome.md
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 7adcd4c12..8a34ae361 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -1136,5 +1136,19 @@
"songs": "Songs",
"time": "Time",
"artists": "Artists"
+ },
+ "dockhand": {
+ "running": "Running",
+ "stopped": "Stopped",
+ "cpu": "CPU",
+ "memory": "Memory",
+ "images": "Images",
+ "volumes": "Volumes",
+ "events_today": "Events Today",
+ "pending_updates": "Pending Updates",
+ "stacks": "Stacks",
+ "paused": "Paused",
+ "total": "Total",
+ "environment_not_found": "Environment Not Found"
}
}
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..50ccff180
--- /dev/null
+++ b/src/widgets/dockhand/component.jsx
@@ -0,0 +1,123 @@
+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 MAX_FIELDS = 4;
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+ const { widget } = service;
+
+ if (!widget.fields) {
+ widget.fields = ["running", "total", "cpu", "memory"];
+ } else if (widget.fields.length > MAX_FIELDS) {
+ widget.fields = widget.fields.slice(0, MAX_FIELDS);
+ }
+
+ const { data: stats, error: statsError } = useWidgetAPI(widget, "dashboard/stats");
+
+ if (statsError) {
+ return