diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 3cdf1b45..4c0c61a3 100755
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -232,6 +232,20 @@
"stopped": "Stopped",
"total": "Total"
},
+ "tailscale": {
+ "address": "Address",
+ "expires": "Expires",
+ "never": "Never",
+ "last_seen": "Last Seen",
+ "now": "Now",
+ "years": "{{number}}y",
+ "weeks": "{{number}}w",
+ "days": "{{number}}d",
+ "hours": "{{number}}h",
+ "minutes": "{{number}}m",
+ "seconds": "{{number}}s",
+ "ago": "{{value}} Ago"
+ },
"tdarr": {
"queue": "Queue",
"processed": "Processed",
diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js
index 93cdb995..5d4b7e3b 100644
--- a/src/utils/proxy/handlers/credentialed.js
+++ b/src/utils/proxy/handlers/credentialed.js
@@ -32,6 +32,7 @@ export default async function credentialedProxyHandler(req, res, map) {
"authentik",
"cloudflared",
"ghostfolio",
+ "tailscale",
"truenas",
"pterodactyl",
].includes(widget.type))
diff --git a/src/widgets/components.js b/src/widgets/components.js
index f8828f3b..c909bfe0 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -71,6 +71,7 @@ const components = {
sonarr: dynamic(() => import("./sonarr/component")),
speedtest: dynamic(() => import("./speedtest/component")),
strelaysrv: dynamic(() => import("./strelaysrv/component")),
+ tailscale: dynamic(() => import("./tailscale/component")),
tautulli: dynamic(() => import("./tautulli/component")),
tdarr: dynamic(() => import("./tdarr/component")),
traefik: dynamic(() => import("./traefik/component")),
diff --git a/src/widgets/tailscale/component.jsx b/src/widgets/tailscale/component.jsx
new file mode 100644
index 00000000..3929b2ed
--- /dev/null
+++ b/src/widgets/tailscale/component.jsx
@@ -0,0 +1,72 @@
+import { useTranslation } from "next-i18next";
+
+import Container from "components/services/widget/container";
+import Block from "components/services/widget/block";
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+
+ const { widget } = service;
+
+ const { data: statsData, error: statsError } = useWidgetAPI(widget, "device");
+
+ if (statsError) {
+ return ;
+ }
+
+ if (!statsData) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const {
+ addresses: [address],
+ keyExpiryDisabled,
+ lastSeen,
+ expires,
+ } = statsData;
+
+ const now = new Date();
+ const compareDifferenceInTwoDates = (priorDate, futureDate) => {
+ const diff = futureDate.getTime() - priorDate.getTime();
+ const diffInYears = Math.ceil(diff / (1000 * 60 * 60 * 24 * 365));
+ if (diffInYears > 1) return t("tailscale.years", { number: diffInYears });
+ const diffInWeeks = Math.ceil(diff / (1000 * 60 * 60 * 24 * 7));
+ if (diffInWeeks > 1) return t("tailscale.weeks", { number: diffInWeeks });
+ const diffInDays = Math.ceil(diff / (1000 * 60 * 60 * 24));
+ if (diffInDays > 1) return t("tailscale.days", { number: diffInDays });
+ const diffInHours = Math.ceil(diff / (1000 * 60 * 60));
+ if (diffInHours > 1) return t("tailscale.hours", { number: diffInHours });
+ const diffInMinutes = Math.ceil(diff / (1000 * 60));
+ if (diffInMinutes > 1) return t("tailscale.minutes", { number: diffInMinutes });
+ const diffInSeconds = Math.ceil(diff / 1000);
+ if (diffInSeconds > 10) return t("tailscale.seconds", { number: diffInSeconds });
+ return "Now";
+ };
+
+ const getLastSeen = () => {
+ const date = new Date(lastSeen);
+ const diff = compareDifferenceInTwoDates(date, now);
+ return diff === "Now" ? t("tailscale.now") : t("tailscale.ago", { value: diff });
+ };
+
+ const getExpiry = () => {
+ if (keyExpiryDisabled) return t("tailscale.never");
+ const date = new Date(expires);
+ return compareDifferenceInTwoDates(now, date);
+ };
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/tailscale/widget.js b/src/widgets/tailscale/widget.js
new file mode 100644
index 00000000..a6d9e864
--- /dev/null
+++ b/src/widgets/tailscale/widget.js
@@ -0,0 +1,14 @@
+import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
+
+const widget = {
+ api: "https://api.tailscale.com/api/v2/{endpoint}/{deviceid}",
+ proxyHandler: credentialedProxyHandler,
+
+ mappings: {
+ device: {
+ endpoint: "device",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 9e155383..20f36a2b 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -65,6 +65,7 @@ import scrutiny from "./scrutiny/widget";
import sonarr from "./sonarr/widget";
import speedtest from "./speedtest/widget";
import strelaysrv from "./strelaysrv/widget";
+import tailscale from "./tailscale/widget";
import tautulli from "./tautulli/widget";
import tdarr from "./tdarr/widget";
import traefik from "./traefik/widget";
@@ -147,6 +148,7 @@ const widgets = {
sonarr,
speedtest,
strelaysrv,
+ tailscale,
tautulli,
tdarr,
traefik,