diff --git a/docs/widgets/services/komodo.md b/docs/widgets/services/komodo.md
new file mode 100644
index 000000000..9eb93de86
--- /dev/null
+++ b/docs/widgets/services/komodo.md
@@ -0,0 +1,22 @@
+---
+title: Komodo
+description: Komodo Widget Configuration
+---
+
+This widget shows either details about all containers or stacks (if `showStacks` is true) managed by [Komodo](https://komo.do/) or the number of running servers, containers and stacks when `showSummary` is enabled.
+
+The api key and secret can be found in the Komodo settings.
+
+Allowed fields (max 4): `["total", "running", "stopped", "unhealthy", "unknown"]`.
+Allowed fields with `showStacks` (max 4): `["total", "running", "down", "unhealthy", "unknown"]`.
+Allowed fields with `showSummary`: `["servers", "stacks", "containers"]`.
+
+```yaml
+widget:
+ type: komodo
+ url: http://komodo.hostname.or.ip:port
+ key: K-xxxxxx...
+ secret: S-xxxxxx...
+ showSummary: true # optional, default: false
+ showStacks: true # optional, default: false
+```
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 43378ec1f..07c12d5a8 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -1060,5 +1060,16 @@
"checkmk": {
"serviceErrors": "Service issues",
"hostErrors": "Host issues"
+ },
+ "komodo": {
+ "total": "Total",
+ "running": "Running",
+ "stopped": "Stopped",
+ "down": "Down",
+ "unhealthy": "Unhealthy",
+ "unknown": "Unknown",
+ "servers": "Servers",
+ "stacks": "Stacks",
+ "containers": "Containers"
}
}
diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js
index 79e3a489b..d46c86057 100644
--- a/src/utils/config/service-helpers.js
+++ b/src/utils/config/service-helpers.js
@@ -338,6 +338,10 @@ export function cleanServiceGroups(groups) {
// jellystat
days,
+ // komodo
+ showSummary,
+ showStacks,
+
// kopia
snapshotHost,
snapshotPath,
@@ -450,6 +454,10 @@ export function cleanServiceGroups(groups) {
if (type === "proxmoxbackupserver") {
if (datastore) widget.datastore = datastore;
}
+ if (type === "komodo") {
+ if (showSummary !== undefined) widget.showSummary = !!JSON.parse(showSummary);
+ if (showStacks !== undefined) widget.showStacks = !!JSON.parse(showStacks);
+ }
if (type === "kubernetes") {
if (namespace) widget.namespace = namespace;
if (app) widget.app = app;
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 92e057dc9..b652c2d86 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -63,6 +63,7 @@ const components = {
jellystat: dynamic(() => import("./jellystat/component")),
kavita: dynamic(() => import("./kavita/component")),
komga: dynamic(() => import("./komga/component")),
+ komodo: dynamic(() => import("./komodo/component")),
kopia: dynamic(() => import("./kopia/component")),
lidarr: dynamic(() => import("./lidarr/component")),
linkwarden: dynamic(() => import("./linkwarden/component")),
diff --git a/src/widgets/komodo/component.jsx b/src/widgets/komodo/component.jsx
new file mode 100644
index 000000000..4d22758e5
--- /dev/null
+++ b/src/widgets/komodo/component.jsx
@@ -0,0 +1,84 @@
+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_ALLOWED_FIELDS = 4;
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+ const { widget } = service;
+ const containersEndpoint = !(!widget.showSummary && widget.showStacks) ? "containers" : "";
+ const { data: containersData, error: containersError } = useWidgetAPI(widget, containersEndpoint);
+ const stacksEndpoint = widget.showSummary || widget.showStacks ? "stacks" : "";
+ const { data: stacksData, error: stacksError } = useWidgetAPI(widget, stacksEndpoint);
+ const serversEndpoint = widget.showSummary ? "servers" : "";
+ const { data: serversData, error: serversError } = useWidgetAPI(widget, serversEndpoint);
+
+ if (containersError || stacksError || serversError) {
+ return ;
+ }
+
+ if (!widget.fields || widget.fields.length === 0) {
+ widget.fields = widget.showSummary
+ ? ["servers", "stacks", "containers"]
+ : widget.showStacks
+ ? ["total", "running", "down", "unhealthy"]
+ : ["total", "running", "stopped", "unhealthy"];
+ } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
+ widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
+ }
+
+ if (
+ (!widget.showStacks && !containersData) ||
+ (widget.showSummary && (!stacksData || !serversData)) ||
+ (widget.showStacks && !stacksData)
+ ) {
+ return widget.showSummary ? (
+
+
+
+
+
+ ) : widget.showStacks ? (
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+ );
+ }
+
+ return widget.showSummary ? (
+
+
+
+
+
+ ) : widget.showStacks ? (
+
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/komodo/proxy.js b/src/widgets/komodo/proxy.js
new file mode 100644
index 000000000..f3c86b2e6
--- /dev/null
+++ b/src/widgets/komodo/proxy.js
@@ -0,0 +1,55 @@
+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("komodoProxyHandler");
+
+export default async function komodoProxyHandler(req, res) {
+ const { group, service, endpoint, index } = req.query;
+
+ if (group && service) {
+ const widget = await getServiceWidget(group, service, index);
+ if (!widgets?.[widget.type]?.api) {
+ return res.status(403).json({ error: "Service does not support API calls" });
+ }
+
+ if (widget) {
+ // api uses unified read endpoint
+ const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint: "read", ...widget })).toString();
+
+ const headers = {
+ "Content-Type": "application/json",
+ "X-API-Key": `${widget.key}`,
+ "X-API-Secret": `${widget.secret}`,
+ };
+ const [status, contentType, data] = await httpProxy(url, {
+ method: "POST",
+ body: JSON.stringify(widgets[widget.type].mappings?.[endpoint]?.body || {}),
+ headers,
+ });
+
+ let resultData = data;
+
+ if (status >= 400) {
+ logger.error("HTTP Error %d calling %s", status, sanitizeErrorURL(url));
+ }
+
+ if (status === 200) {
+ if (!validateWidgetData(widget, endpoint, resultData)) {
+ return res
+ .status(500)
+ .json({ error: { message: "Invalid data", url: sanitizeErrorURL(url), data: resultData } });
+ }
+ }
+
+ if (contentType) res.setHeader("Content-Type", contentType);
+ return res.status(status).send(resultData);
+ }
+ }
+
+ logger.debug("Invalid or missing proxy service type '%s' in group '%s'", service, group);
+ return res.status(400).json({ error: "Invalid proxy service type" });
+}
diff --git a/src/widgets/komodo/widget.js b/src/widgets/komodo/widget.js
new file mode 100644
index 000000000..55454f97c
--- /dev/null
+++ b/src/widgets/komodo/widget.js
@@ -0,0 +1,32 @@
+import komodoProxyHandler from "./proxy";
+
+const widget = {
+ api: "{url}/{endpoint}",
+ proxyHandler: komodoProxyHandler,
+
+ mappings: {
+ containers: {
+ endpoint: "containers", // api actually uses unified read endpoint
+ body: {
+ type: "GetDockerContainersSummary",
+ params: {},
+ },
+ },
+ stacks: {
+ endpoint: "stacks", // api actually uses unified read endpoint
+ body: {
+ type: "GetStacksSummary",
+ params: {},
+ },
+ },
+ servers: {
+ endpoint: "servers", // api actually uses unified read endpoint
+ body: {
+ type: "GetServersSummary",
+ params: {},
+ },
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 2a2df8c23..8178f26ba 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -54,6 +54,7 @@ import jellystat from "./jellystat/widget";
import karakeep from "./karakeep/widget";
import kavita from "./kavita/widget";
import komga from "./komga/widget";
+import komodo from "./komodo/widget";
import kopia from "./kopia/widget";
import lidarr from "./lidarr/widget";
import linkwarden from "./linkwarden/widget";
@@ -197,6 +198,7 @@ const widgets = {
jellystat,
kavita,
komga,
+ komodo,
kopia,
lidarr,
linkwarden,