diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index 52db2cb4..f144182f 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -447,5 +447,12 @@
"photos": "Photos",
"videos": "Videos",
"storage": "Storage"
+ },
+ "uptimekuma": {
+ "up": "Sites Up",
+ "down": "Sites Down",
+ "uptime": "Uptime",
+ "incident": "Incident",
+ "m": "m"
}
-}
+}
\ No newline at end of file
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 43a46fa9..505807c4 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -63,6 +63,7 @@ const components = {
watchtower: dynamic(() => import("./watchtower/component")),
xteve: dynamic(() => import("./xteve/component")),
immich: dynamic(() => import("./immich/component")),
+ uptimekuma: dynamic(() => import("./uptimekuma/component")),
};
export default components;
diff --git a/src/widgets/uptimekuma/component.jsx b/src/widgets/uptimekuma/component.jsx
new file mode 100644
index 00000000..d71f9a63
--- /dev/null
+++ b/src/widgets/uptimekuma/component.jsx
@@ -0,0 +1,55 @@
+import { useTranslation } from "next-i18next";
+
+import Container from "components/services/widget/container";
+import useWidgetAPI from "utils/proxy/use-widget-api";
+import Block from "components/services/widget/block";
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+
+ const { widget } = service;
+
+ const { data: statusData, error: statusError } = useWidgetAPI(widget, "status_page");
+ const { data: heartbeatData, error: heartbeatError } = useWidgetAPI(widget, "heartbeat");
+
+ if (statusError || heartbeatError) {
+ return ;
+ }
+
+ if (!statusData || !heartbeatData) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ let sitesUp = 0;
+ let sitesDown = 0;
+ Object.values(heartbeatData.heartbeatList).forEach((siteList) => {
+ const lastHeartbeat = siteList[siteList.length - 1];
+ if (lastHeartbeat?.status === 1) {
+ sitesUp += 1;
+ } else {
+ sitesDown += 1;
+ }
+ });
+
+ // Adapted from https://github.com/bastienwirtz/homer/blob/b7cd8f9482e6836a96b354b11595b03b9c3d67cd/src/components/services/UptimeKuma.vue#L105
+ const uptimeList = Object.values(heartbeatData.uptimeList);
+ const percent = uptimeList.reduce((a, b) => a + b, 0) / uptimeList.length || 0;
+ const uptime = (percent * 100).toFixed(1);
+ const incidentTime = statusData.incident ? (Math.abs(new Date(statusData.incident?.createdDate) - new Date()) / 1000) / (60 * 60) : null;
+
+ return (
+
+
+
+
+ {incidentTime && }
+
+ );
+}
diff --git a/src/widgets/uptimekuma/widget.js b/src/widgets/uptimekuma/widget.js
new file mode 100644
index 00000000..928534b3
--- /dev/null
+++ b/src/widgets/uptimekuma/widget.js
@@ -0,0 +1,18 @@
+// import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
+import genericProxyHandler from "utils/proxy/handlers/generic";
+
+const widget = {
+ api: "{url}/api/{endpoint}/{slug}",
+ proxyHandler: genericProxyHandler,
+
+ mappings: {
+ status_page: {
+ endpoint: "status-page",
+ },
+ heartbeat: {
+ endpoint: "status-page/heartbeat",
+ },
+ }
+};
+
+export default widget;
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 133903fb..7da77a0a 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -57,6 +57,7 @@ import unifi from "./unifi/widget";
import watchtower from "./watchtower/widget";
import xteve from "./xteve/widget";
import immich from "./immich/widget";
+import uptimekuma from "./uptimekuma/widget";
const widgets = {
adguard,
@@ -121,6 +122,7 @@ const widgets = {
watchtower,
xteve,
immich,
+ uptimekuma,
};
export default widgets;