diff --git a/docs/widgets/services/pangolin.md b/docs/widgets/services/pangolin.md
new file mode 100644
index 000000000..ab5cb44d5
--- /dev/null
+++ b/docs/widgets/services/pangolin.md
@@ -0,0 +1,29 @@
+---
+title: Pangolin
+description: Pangolin Widget Configuration
+---
+
+Learn more about [Pangolin](https://github.com/fosrl/pangolin).
+
+This widget shows sites (online/total), resources (healthy/total), targets (healthy/total), and traffic statistics for a Pangolin organization. A resource is considered healthy if at least one of its targets is healthy, or if it has no targets.
+
+Allowed fields: `["sites", "resources", "targets", "traffic", "in", "out"]` (maximum of 4).
+
+```yaml
+widget:
+ type: pangolin
+ url: https://api.pangolin.net
+ key: your-api-key
+ org: your-org-id
+```
+
+Find your organization ID in the URL when logged in (e.g., `https://app.pangolin.net/{org-id}/...`).
+
+## API Key Setup
+
+Create an API key with the following permissions:
+
+- **List Sites**
+- **List Resources**
+
+**Self-Hosted:** Enable the [Integration API](https://docs.pangolin.net/self-host/advanced/integration-api) in your Pangolin configuration before creating the key.
diff --git a/mkdocs.yml b/mkdocs.yml
index 8be043596..b3865d3c0 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -122,6 +122,7 @@ nav:
- widgets/services/opnsense.md
- widgets/services/openwrt.md
- widgets/services/overseerr.md
+ - widgets/services/pangolin.md
- widgets/services/paperlessngx.md
- widgets/services/peanut.md
- widgets/services/pfsense.md
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 05582127c..2d237a23c 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -599,6 +599,15 @@
"inbox": "Inbox",
"total": "Total"
},
+ "pangolin": {
+ "orgs": "Orgs",
+ "sites": "Sites",
+ "resources": "Resources",
+ "targets": "Targets",
+ "traffic": "Traffic",
+ "in": "In",
+ "out": "Out"
+ },
"peanut": {
"battery_charge": "Battery Charge",
"ups_load": "UPS Load",
diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js
index e92dddaed..f5e9cf51a 100644
--- a/src/utils/proxy/handlers/credentialed.js
+++ b/src/utils/proxy/handlers/credentialed.js
@@ -53,6 +53,7 @@ export default async function credentialedProxyHandler(req, res, map) {
"linkwarden",
"mealie",
"netalertx",
+ "pangolin",
"tailscale",
"tandoor",
"pterodactyl",
diff --git a/src/widgets/components.js b/src/widgets/components.js
index d6b150b85..911be5fb8 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -97,6 +97,7 @@ const components = {
openmediavault: dynamic(() => import("./openmediavault/component")),
openwrt: dynamic(() => import("./openwrt/component")),
paperlessngx: dynamic(() => import("./paperlessngx/component")),
+ pangolin: dynamic(() => import("./pangolin/component")),
pfsense: dynamic(() => import("./pfsense/component")),
photoprism: dynamic(() => import("./photoprism/component")),
proxmoxbackupserver: dynamic(() => import("./proxmoxbackupserver/component")),
diff --git a/src/widgets/pangolin/component.jsx b/src/widgets/pangolin/component.jsx
new file mode 100644
index 000000000..5e81eecca
--- /dev/null
+++ b/src/widgets/pangolin/component.jsx
@@ -0,0 +1,70 @@
+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;
+
+ if (!widget.fields) {
+ widget.fields = ["sites", "resources", "targets", "traffic"];
+ } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
+ widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
+ }
+
+ const { data: sitesData, error: sitesError } = useWidgetAPI(widget, "sites");
+ const { data: resourcesData, error: resourcesError } = useWidgetAPI(widget, "resources");
+
+ if (sitesError || resourcesError) {
+ return ;
+ }
+
+ if (!sitesData || !resourcesData) {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+ const sites = sitesData.data?.sites || [];
+ const resources = resourcesData.data?.resources || [];
+
+ const sitesTotal = sites.length;
+ const sitesOnline = sites.filter((s) => s.online).length;
+
+ const resourcesTotal = resources.length;
+ const resourcesHealthy = resources.filter(
+ (r) => r.targets?.some((t) => t.healthStatus !== "unhealthy") || !r.targets?.length,
+ ).length;
+
+ const targetsTotal = resources.reduce((sum, r) => sum + (r.targets?.length || 0), 0);
+ const targetsHealthy = resources.reduce(
+ (sum, r) => sum + (r.targets?.filter((t) => t.healthStatus !== "unhealthy").length || 0),
+ 0,
+ );
+
+ const trafficIn = sites.reduce((sum, s) => sum + (s.megabytesIn || 0), 0) * 1_000_000;
+ const trafficOut = sites.reduce((sum, s) => sum + (s.megabytesOut || 0), 0) * 1_000_000;
+ const trafficTotal = trafficIn + trafficOut;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/pangolin/widget.js b/src/widgets/pangolin/widget.js
new file mode 100644
index 000000000..899949fa6
--- /dev/null
+++ b/src/widgets/pangolin/widget.js
@@ -0,0 +1,17 @@
+import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
+
+const widget = {
+ api: "{url}/v1/{endpoint}",
+ proxyHandler: credentialedProxyHandler,
+
+ mappings: {
+ sites: {
+ endpoint: "org/{org}/sites",
+ },
+ resources: {
+ endpoint: "org/{org}/resources",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 90cd7bfc8..dcc0ba65e 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -87,6 +87,7 @@ import openmediavault from "./openmediavault/widget";
import openwrt from "./openwrt/widget";
import opnsense from "./opnsense/widget";
import overseerr from "./overseerr/widget";
+import pangolin from "./pangolin/widget";
import paperlessngx from "./paperlessngx/widget";
import peanut from "./peanut/widget";
import pfsense from "./pfsense/widget";
@@ -237,6 +238,7 @@ const widgets = {
openmediavault,
openwrt,
paperlessngx,
+ pangolin,
peanut,
pfsense,
photoprism,