@@ -49,7 +49,7 @@ export default function Widget({ options }) {
stock && (
{stock.ticker.split(":").pop()}
diff --git a/src/components/widgets/unifi_console/unifi_console.jsx b/src/components/widgets/unifi_console/unifi_console.jsx
index 5295dbb7..09e6952f 100644
--- a/src/components/widgets/unifi_console/unifi_console.jsx
+++ b/src/components/widgets/unifi_console/unifi_console.jsx
@@ -1,13 +1,13 @@
-import { BiError, BiWifi, BiCheckCircle, BiXCircle, BiNetworkChart } from "react-icons/bi";
-import { MdSettingsEthernet } from "react-icons/md";
import { useTranslation } from "next-i18next";
+import { BiCheckCircle, BiError, BiNetworkChart, BiWifi, BiXCircle } from "react-icons/bi";
+import { MdSettingsEthernet } from "react-icons/md";
import { SiUbiquiti } from "react-icons/si";
-import Error from "../widget/error";
import Container from "../widget/container";
+import Error from "../widget/error";
+import PrimaryText from "../widget/primary_text";
import Raw from "../widget/raw";
import WidgetIcon from "../widget/widget_icon";
-import PrimaryText from "../widget/primary_text";
import useWidgetAPI from "utils/proxy/use-widget-api";
diff --git a/src/components/widgets/weather/weather.jsx b/src/components/widgets/weather/weather.jsx
index 4ebb08c5..98768963 100644
--- a/src/components/widgets/weather/weather.jsx
+++ b/src/components/widgets/weather/weather.jsx
@@ -1,16 +1,16 @@
-import useSWR from "swr";
-import { useState } from "react";
-import { WiCloudDown } from "react-icons/wi";
-import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
import { useTranslation } from "next-i18next";
+import { useState } from "react";
+import { MdLocationDisabled, MdLocationSearching } from "react-icons/md";
+import { WiCloudDown } from "react-icons/wi";
+import useSWR from "swr";
-import Error from "../widget/error";
+import mapIcon from "../../../utils/weather/condition-map";
import Container from "../widget/container";
+import ContainerButton from "../widget/container_button";
+import Error from "../widget/error";
import PrimaryText from "../widget/primary_text";
import SecondaryText from "../widget/secondary_text";
import WidgetIcon from "../widget/widget_icon";
-import ContainerButton from "../widget/container_button";
-import mapIcon from "../../../utils/weather/condition-map";
function Widget({ options }) {
const { t, i18n } = useTranslation();
diff --git a/src/components/widgets/widget.jsx b/src/components/widgets/widget.jsx
index c7f0bf4d..ebc706ac 100644
--- a/src/components/widgets/widget.jsx
+++ b/src/components/widgets/widget.jsx
@@ -1,6 +1,5 @@
-import dynamic from "next/dynamic";
-
import ErrorBoundary from "components/errorboundry";
+import dynamic from "next/dynamic";
const widgetMappings = {
weatherapi: dynamic(() => import("components/widgets/weather/weather")),
diff --git a/src/components/widgets/widget/container.jsx b/src/components/widgets/widget/container.jsx
index 8675adf8..fe6397e0 100644
--- a/src/components/widgets/widget/container.jsx
+++ b/src/components/widgets/widget/container.jsx
@@ -1,13 +1,12 @@
import classNames from "classnames";
import { useContext } from "react";
-
-import WidgetIcon from "./widget_icon";
-import PrimaryText from "./primary_text";
-import SecondaryText from "./secondary_text";
-import Raw from "./raw";
-
import { SettingsContext } from "utils/contexts/settings";
+import PrimaryText from "./primary_text";
+import Raw from "./raw";
+import SecondaryText from "./secondary_text";
+import WidgetIcon from "./widget_icon";
+
export function getAllClasses(options, additionalClassNames = "") {
if (options?.style?.header === "boxedWidgets") {
if (options?.style?.cardBlur !== undefined) {
diff --git a/src/components/widgets/widget/container_button.jsx b/src/components/widgets/widget/container_button.jsx
index a6379081..e0802511 100644
--- a/src/components/widgets/widget/container_button.jsx
+++ b/src/components/widgets/widget/container_button.jsx
@@ -1,4 +1,4 @@
-import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
+import { getAllClasses, getBottomBlock, getInnerBlock } from "./container";
export default function ContainerButton({ children = [], options, additionalClassNames = "", callback }) {
return (
diff --git a/src/components/widgets/widget/container_form.jsx b/src/components/widgets/widget/container_form.jsx
index 68cbd64b..a9afef3a 100644
--- a/src/components/widgets/widget/container_form.jsx
+++ b/src/components/widgets/widget/container_form.jsx
@@ -1,4 +1,4 @@
-import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
+import { getAllClasses, getBottomBlock, getInnerBlock } from "./container";
export default function ContainerForm({ children = [], options, additionalClassNames = "", callback }) {
return (
diff --git a/src/components/widgets/widget/container_link.jsx b/src/components/widgets/widget/container_link.jsx
index 6f157875..a36f311c 100644
--- a/src/components/widgets/widget/container_link.jsx
+++ b/src/components/widgets/widget/container_link.jsx
@@ -1,4 +1,4 @@
-import { getAllClasses, getInnerBlock, getBottomBlock } from "./container";
+import { getAllClasses, getBottomBlock, getInnerBlock } from "./container";
export default function ContainerLink({ children = [], options, additionalClassNames = "", target }) {
return (
diff --git a/src/components/widgets/widget/error.jsx b/src/components/widgets/widget/error.jsx
index e454256f..e0e8b1e1 100644
--- a/src/components/widgets/widget/error.jsx
+++ b/src/components/widgets/widget/error.jsx
@@ -1,4 +1,4 @@
-import { useTranslation } from "react-i18next";
+import { useTranslation } from "next-i18next";
import { BiError } from "react-icons/bi";
import Container from "./container";
diff --git a/src/components/widgets/widget/resources.jsx b/src/components/widgets/widget/resources.jsx
index 2f594942..b9a48a41 100644
--- a/src/components/widgets/widget/resources.jsx
+++ b/src/components/widgets/widget/resources.jsx
@@ -1,8 +1,8 @@
import classNames from "classnames";
import ContainerLink from "./container_link";
-import Resource from "./resource";
import Raw from "./raw";
+import Resource from "./resource";
import WidgetLabel from "./widget_label";
export default function Resources({ options, children, target, additionalClassNames }) {
diff --git a/src/middleware.js b/src/middleware.js
new file mode 100644
index 00000000..bb9fbea5
--- /dev/null
+++ b/src/middleware.js
@@ -0,0 +1,24 @@
+import { NextResponse } from "next/server";
+
+export function middleware(req) {
+ // Check the Host header, if HOMEPAGE_ALLOWED_HOSTS is set
+ const host = req.headers.get("host");
+ const port = process.env.PORT || 3000;
+ let allowedHosts = [`localhost:${port}`, `127.0.0.1:${port}`];
+ const allowAll = process.env.HOMEPAGE_ALLOWED_HOSTS === "*";
+ if (process.env.HOMEPAGE_ALLOWED_HOSTS) {
+ allowedHosts = allowedHosts.concat(process.env.HOMEPAGE_ALLOWED_HOSTS.split(","));
+ }
+ if (!allowAll && (!host || !allowedHosts.includes(host))) {
+ // eslint-disable-next-line no-console
+ console.error(
+ `Host validation failed for: ${host}. Hint: Set the HOMEPAGE_ALLOWED_HOSTS environment variable to allow requests from this host / port.`,
+ );
+ return NextResponse.json({ error: "Host validation failed. See logs for more details." }, { status: 400 });
+ }
+ return NextResponse.next();
+}
+
+export const config = {
+ matcher: "/api/:path*",
+};
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
index 29c04da3..8e88f6b2 100644
--- a/src/pages/_app.jsx
+++ b/src/pages/_app.jsx
@@ -1,17 +1,71 @@
/* eslint-disable react/jsx-props-no-spreading */
-import { SWRConfig } from "swr";
import { appWithTranslation } from "next-i18next";
import Head from "next/head";
-
import "styles/globals.css";
-import "styles/theme.css";
import "styles/manrope.css";
-import nextI18nextConfig from "../../next-i18next.config";
-
+import "styles/theme.css";
+import { SWRConfig } from "swr";
import { ColorProvider } from "utils/contexts/color";
-import { ThemeProvider } from "utils/contexts/theme";
import { SettingsProvider } from "utils/contexts/settings";
import { TabProvider } from "utils/contexts/tab";
+import { ThemeProvider } from "utils/contexts/theme";
+
+import nextI18nextConfig from "../../next-i18next.config";
+
+// eslint-disable-next-line no-unused-vars
+const tailwindSafelist = [
+ // TODO: remove pending https://github.com/tailwindlabs/tailwindcss/pull/17147
+ "backdrop-blur",
+ "backdrop-blur-xs",
+ "backdrop-blur-sm",
+ "backdrop-blur-md",
+ "backdrop-blur-xl",
+ "backdrop-saturate-0",
+ "backdrop-saturate-50",
+ "backdrop-saturate-100",
+ "backdrop-saturate-150",
+ "backdrop-saturate-200",
+ "backdrop-brightness-0",
+ "backdrop-brightness-50",
+ "backdrop-brightness-75",
+ "backdrop-brightness-90",
+ "backdrop-brightness-95",
+ "backdrop-brightness-100",
+ "backdrop-brightness-105",
+ "backdrop-brightness-110",
+ "backdrop-brightness-125",
+ "backdrop-brightness-150",
+ "backdrop-brightness-200",
+ "grid-cols-1",
+ "md:grid-cols-1",
+ "md:grid-cols-2",
+ "lg:grid-cols-1",
+ "lg:grid-cols-2",
+ "lg:grid-cols-3",
+ "lg:grid-cols-4",
+ "lg:grid-cols-5",
+ "lg:grid-cols-6",
+ "lg:grid-cols-7",
+ "lg:grid-cols-8",
+ // for status
+ "bg-white",
+ "bg-black",
+ "dark:bg-white",
+ "bg-orange-400",
+ "dark:bg-orange-400",
+ // maxGroupColumns
+ "3xl:basis-1/5",
+ "3xl:basis-1/6",
+ "3xl:basis-1/7",
+ "3xl:basis-1/8",
+ // yep
+ "h-0 h-1 h-2 h-3 h-4 h-5 h-6 h-7 h-8 h-9 h-10 h-11 h-12 h-13 h-14 h-15 h-16 h-17 h-18 h-19 h-20 h-21 h-22 h-23 h-24 h-25 h-26 h-27 h-28 h-29 h-30 h-31 h-32 h-33 h-34 h-35 h-36 h-37 h-38 h-39 h-40 h-41 h-42 h-43 h-44 h-45 h-46 h-47 h-48 h-49 h-50 h-51 h-52 h-53 h-54 h-55 h-56 h-57 h-58 h-59 h-60 h-61 h-62 h-63 h-64 h-65 h-66 h-67 h-68 h-69 h-70 h-71 h-72 h-73 h-74 h-75 h-76 h-77 h-78 h-79 h-80 h-81 h-82 h-83 h-84 h-85 h-86 h-87 h-88 h-89 h-90 h-91 h-92 h-93 h-94 h-95 h-96",
+ "sm:h-0 sm:h-1 sm:h-2 sm:h-3 sm:h-4 sm:h-5 sm:h-6 sm:h-7 sm:h-8 sm:h-9 sm:h-10 sm:h-11 sm:h-12 sm:h-13 sm:h-14 sm:h-15 sm:h-16 sm:h-17 sm:h-18 sm:h-19 sm:h-20 sm:h-21 sm:h-22 sm:h-23 sm:h-24 sm:h-25 sm:h-26 sm:h-27 sm:h-28 sm:h-29 sm:h-30 sm:h-31 sm:h-32 sm:h-33 sm:h-34 sm:h-35 sm:h-36 sm:h-37 sm:h-38 sm:h-39 sm:h-40 sm:h-41 sm:h-42 sm:h-43 sm:h-44 sm:h-45 sm:h-46 sm:h-47 sm:h-48 sm:h-49 sm:h-50 sm:h-51 sm:h-52 sm:h-53 sm:h-54 sm:h-55 sm:h-56 sm:h-57 sm:h-58 sm:h-59 sm:h-60 sm:h-61 sm:h-62 sm:h-63 sm:h-64 sm:h-65 sm:h-66 sm:h-67 sm:h-68 sm:h-69 sm:h-70 sm:h-71 sm:h-72 sm:h-73 sm:h-74 sm:h-75 sm:h-76 sm:h-77 sm:h-78 sm:h-79 sm:h-80 sm:h-81 sm:h-82 sm:h-83 sm:h-84 sm:h-85 sm:h-86 sm:h-87 sm:h-88 sm:h-89 sm:h-90 sm:h-91 sm:h-92 sm:h-93 sm:h-94 sm:h-95 sm:h-96",
+ "md:h-0 md:h-1 md:h-2 md:h-3 md:h-4 md:h-5 md:h-6 md:h-7 md:h-8 md:h-9 md:h-10 md:h-11 md:h-12 md:h-13 md:h-14 md:h-15 md:h-16 md:h-17 md:h-18 md:h-19 md:h-20 md:h-21 md:h-22 md:h-23 md:h-24 md:h-25 md:h-26 md:h-27 md:h-28 md:h-29 md:h-30 md:h-31 md:h-32 md:h-33 md:h-34 md:h-35 md:h-36 md:h-37 md:h-38 md:h-39 md:h-40 md:h-41 md:h-42 md:h-43 md:h-44 md:h-45 md:h-46 md:h-47 md:h-48 md:h-49 md:h-50 md:h-51 md:h-52 md:h-53 md:h-54 md:h-55 md:h-56 md:h-57 md:h-58 md:h-59 md:h-60 md:h-61 md:h-62 md:h-63 md:h-64 md:h-65 md:h-66 md:h-67 md:h-68 md:h-69 md:h-70 md:h-71 md:h-72 md:h-73 md:h-74 md:h-75 md:h-76 md:h-77 md:h-78 md:h-79 md:h-80 md:h-81 md:h-82 md:h-83 md:h-84 md:h-85 md:h-86 md:h-87 md:h-88 md:h-89 md:h-90 md:h-91 md:h-92 md:h-93 md:h-94 md:h-95 md:h-96",
+ "lg:h-0 lg:h-1 lg:h-2 lg:h-3 lg:h-4 lg:h-5 lg:h-6 lg:h-7 lg:h-8 lg:h-9 lg:h-10 lg:h-11 lg:h-12 lg:h-13 lg:h-14 lg:h-15 lg:h-16 lg:h-17 lg:h-18 lg:h-19 lg:h-20 lg:h-21 lg:h-22 lg:h-23 lg:h-24 lg:h-25 lg:h-26 lg:h-27 lg:h-28 lg:h-29 lg:h-30 lg:h-31 lg:h-32 lg:h-33 lg:h-34 lg:h-35 lg:h-36 lg:h-37 lg:h-38 lg:h-39 lg:h-40 lg:h-41 lg:h-42 lg:h-43 lg:h-44 lg:h-45 lg:h-46 lg:h-47 lg:h-48 lg:h-49 lg:h-50 lg:h-51 lg:h-52 lg:h-53 lg:h-54 lg:h-55 lg:h-56 lg:h-57 lg:h-58 lg:h-59 lg:h-60 lg:h-61 lg:h-62 lg:h-63 lg:h-64 lg:h-65 lg:h-66 lg:h-67 lg:h-68 lg:h-69 lg:h-70 lg:h-71 lg:h-72 lg:h-73 lg:h-74 lg:h-75 lg:h-76 lg:h-77 lg:h-78 lg:h-79 lg:h-80 lg:h-81 lg:h-82 lg:h-83 lg:h-84 lg:h-85 lg:h-86 lg:h-87 lg:h-88 lg:h-89 lg:h-90 lg:h-91 lg:h-92 lg:h-93 lg:h-94 lg:h-95 lg:h-96",
+ "xl:h-0 xl:h-1 xl:h-2 xl:h-3 xl:h-4 xl:h-5 xl:h-6 xl:h-7 xl:h-8 xl:h-9 xl:h-10 xl:h-11 xl:h-12 xl:h-13 xl:h-14 xl:h-15 xl:h-16 xl:h-17 xl:h-18 xl:h-19 xl:h-20 xl:h-21 xl:h-22 xl:h-23 xl:h-24 xl:h-25 xl:h-26 xl:h-27 xl:h-28 xl:h-29 xl:h-30 xl:h-31 xl:h-32 xl:h-33 xl:h-34 xl:h-35 xl:h-36 xl:h-37 xl:h-38 xl:h-39 xl:h-40 xl:h-41 xl:h-42 xl:h-43 xl:h-44 xl:h-45 xl:h-46 xl:h-47 xl:h-48 xl:h-49 xl:h-50 xl:h-51 xl:h-52 xl:h-53 xl:h-54 xl:h-55 xl:h-56 xl:h-57 xl:h-58 xl:h-59 xl:h-60 xl:h-61 xl:h-62 xl:h-63 xl:h-64 xl:h-65 xl:h-66 xl:h-67 xl:h-68 xl:h-69 xl:h-70 xl:h-71 xl:h-72 xl:h-73 xl:h-74 xl:h-75 xl:h-76 xl:h-77 xl:h-78 xl:h-79 xl:h-80 xl:h-81 xl:h-82 xl:h-83 xl:h-84 xl:h-85 xl:h-86 xl:h-87 xl:h-88 xl:h-89 xl:h-90 xl:h-91 xl:h-92 xl:h-93 xl:h-94 xl:h-95 xl:h-96",
+ "2xl:h-0 2xl:h-1 2xl:h-2 2xl:h-3 2xl:h-4 2xl:h-5 2xl:h-6 2xl:h-7 2xl:h-8 2xl:h-9 2xl:h-10 2xl:h-11 2xl:h-12 2xl:h-13 2xl:h-14 2xl:h-15 2xl:h-16 2xl:h-17 2xl:h-18 2xl:h-19 2xl:h-20 2xl:h-21 2xl:h-22 2xl:h-23 2xl:h-24 2xl:h-25 2xl:h-26 2xl:h-27 2xl:h-28 2xl:h-29 2xl:h-30 2xl:h-31 2xl:h-32 2xl:h-33 2xl:h-34 2xl:h-35 2xl:h-36 2xl:h-37 2xl:h-38 2xl:h-39 2xl:h-40 2xl:h-41 2xl:h-42 2xl:h-43 2xl:h-44 2xl:h-45 2xl:h-46 2xl:h-47 2xl:h-48 2xl:h-49 2xl:h-50 2xl:h-51 2xl:h-52 2xl:h-53 2xl:h-54 2xl:h-55 2xl:h-56 2xl:h-57 2xl:h-58 2xl:h-59 2xl:h-60 2xl:h-61 2xl:h-62 2xl:h-63 2xl:h-64 2xl:h-65 2xl:h-66 2xl:h-67 2xl:h-68 2xl:h-69 2xl:h-70 2xl:h-71 2xl:h-72 2xl:h-73 2xl:h-74 2xl:h-75 2xl:h-76 2xl:h-77 2xl:h-78 2xl:h-79 2xl:h-80 2xl:h-81 2xl:h-82 2xl:h-83 2xl:h-84 2xl:h-85 2xl:h-86 2xl:h-87 2xl:h-88 2xl:h-89 2xl:h-90 2xl:h-91 2xl:h-92 2xl:h-93 2xl:h-94 2xl:h-95 2xl:h-96",
+];
function MyApp({ Component, pageProps }) {
return (
diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx
index 31083438..ece38aec 100644
--- a/src/pages/_document.jsx
+++ b/src/pages/_document.jsx
@@ -1,4 +1,4 @@
-import { Html, Head, Main, NextScript } from "next/document";
+import { Head, Html, Main, NextScript } from "next/document";
export default function Document() {
return (
diff --git a/src/pages/api/auth.js b/src/pages/api/auth.js
new file mode 100644
index 00000000..9f79c930
--- /dev/null
+++ b/src/pages/api/auth.js
@@ -0,0 +1,17 @@
+import { checkAllowedGroup, readIdentitySettings } from "utils/identity/identity-helpers";
+import { getSettings } from "utils/config/config";
+
+export default async function handler(req, res) {
+ const { group } = req.query;
+ const { provider, groups } = readIdentitySettings(getSettings().identity);
+
+ try {
+ if (checkAllowedGroup(provider.getIdentity(req), groups, group)) {
+ res.json({ group });
+ } else {
+ res.status(401).json({ message: "Group unathorized" });
+ }
+ } catch (err) {
+ res.status(500).send("Error getting user identity");
+ }
+}
diff --git a/src/pages/api/bookmarks.js b/src/pages/api/bookmarks.js
index 63d1e29e..cf8bc09d 100644
--- a/src/pages/api/bookmarks.js
+++ b/src/pages/api/bookmarks.js
@@ -1,5 +1,8 @@
+import { readIdentitySettings } from "utils/identity/identity-helpers";
import { bookmarksResponse } from "utils/config/api-response";
+import { getSettings } from "utils/config/config";
export default async function handler(req, res) {
- res.send(await bookmarksResponse());
+ const { provider, groups } = readIdentitySettings(getSettings().identity);
+ res.send(await bookmarksResponse(provider.getIdentity(req), groups));
}
diff --git a/src/pages/api/config/[path].js b/src/pages/api/config/[path].js
index 6cb04698..b69ddff5 100644
--- a/src/pages/api/config/[path].js
+++ b/src/pages/api/config/[path].js
@@ -1,5 +1,5 @@
-import path from "path";
import fs from "fs";
+import path from "path";
import { CONF_DIR } from "utils/config/config";
import createLogger from "utils/logger";
diff --git a/src/pages/api/hash.js b/src/pages/api/hash.js
index 992f9ea6..33fb4ef5 100644
--- a/src/pages/api/hash.js
+++ b/src/pages/api/hash.js
@@ -1,6 +1,6 @@
-import { join } from "path";
import { createHash } from "crypto";
import { readFileSync } from "fs";
+import { join } from "path";
import checkAndCopyConfig, { CONF_DIR } from "utils/config/config";
diff --git a/src/pages/api/kubernetes/stats/[...service].js b/src/pages/api/kubernetes/stats/[...service].js
index b1bf8345..ab454183 100644
--- a/src/pages/api/kubernetes/stats/[...service].js
+++ b/src/pages/api/kubernetes/stats/[...service].js
@@ -1,7 +1,7 @@
import { CoreV1Api, Metrics } from "@kubernetes/client-node";
-import getKubeConfig from "../../../../utils/config/kubernetes";
-import { parseCpu, parseMemory } from "../../../../utils/kubernetes/kubernetes-utils";
+import { getKubeConfig } from "../../../../utils/config/kubernetes";
+import { parseCpu, parseMemory } from "../../../../utils/kubernetes/utils";
import createLogger from "../../../../utils/logger";
const logger = createLogger("kubernetesStatsService");
@@ -30,8 +30,10 @@ export default async function handler(req, res) {
const coreApi = kc.makeApiClient(CoreV1Api);
const metricsApi = new Metrics(kc);
const podsResponse = await coreApi
- .listNamespacedPod(namespace, null, null, null, null, labelSelector)
- .then((response) => response.body)
+ .listNamespacedPod({
+ namespace,
+ labelSelector,
+ })
.catch((err) => {
logger.error("Error getting pods: %d %s %s", err.statusCode, err.body, err.response);
return null;
@@ -51,9 +53,11 @@ export default async function handler(req, res) {
return;
}
+ const podNames = new Set();
let cpuLimit = 0;
let memLimit = 0;
pods.forEach((pod) => {
+ podNames.add(pod.metadata.name);
pod.spec.containers.forEach((container) => {
if (container?.resources?.limits?.cpu) {
cpuLimit += parseCpu(container?.resources?.limits?.cpu);
@@ -64,40 +68,32 @@ export default async function handler(req, res) {
});
});
- const podStatsList = await Promise.all(
- pods.map(async (pod) => {
- let depMem = 0;
- let depCpu = 0;
- const podMetrics = await metricsApi
- .getPodMetrics(namespace, pod.metadata.name)
- .then((response) => response)
- .catch((err) => {
- // 404 generally means that the metrics have not been populated yet
- if (err.statusCode !== 404) {
- logger.error("Error getting pod metrics: %d %s %s", err.statusCode, err.body, err.response);
- }
- return null;
- });
- if (podMetrics) {
- podMetrics.containers.forEach((container) => {
- depMem += parseMemory(container.usage.memory);
- depCpu += parseCpu(container.usage.cpu);
- });
+ const namespaceMetrics = await metricsApi
+ .getPodMetrics(namespace)
+ .then((response) => response.items)
+ .catch((err) => {
+ // 404 generally means that the metrics have not been populated yet
+ if (err.statusCode !== 404) {
+ logger.error("Error getting pod metrics: %d %s %s", err.statusCode, err.body, err.response);
}
- return {
- mem: depMem,
- cpu: depCpu,
- };
- }),
- );
+ return null;
+ });
+
const stats = {
mem: 0,
cpu: 0,
};
- podStatsList.forEach((podStat) => {
- stats.mem += podStat.mem;
- stats.cpu += podStat.cpu;
- });
+
+ if (namespaceMetrics) {
+ const podMetrics = namespaceMetrics.filter((item) => podNames.has(item.metadata.name));
+ podMetrics.forEach((metrics) => {
+ metrics.containers.forEach((container) => {
+ stats.mem += parseMemory(container.usage.memory);
+ stats.cpu += parseCpu(container.usage.cpu);
+ });
+ });
+ }
+
stats.cpuLimit = cpuLimit;
stats.memLimit = memLimit;
stats.cpuUsage = cpuLimit ? 100 * (stats.cpu / cpuLimit) : 0;
diff --git a/src/pages/api/kubernetes/status/[...service].js b/src/pages/api/kubernetes/status/[...service].js
index e50d726c..e8558924 100644
--- a/src/pages/api/kubernetes/status/[...service].js
+++ b/src/pages/api/kubernetes/status/[...service].js
@@ -1,6 +1,6 @@
import { CoreV1Api } from "@kubernetes/client-node";
-import getKubeConfig from "../../../../utils/config/kubernetes";
+import { getKubeConfig } from "../../../../utils/config/kubernetes";
import createLogger from "../../../../utils/logger";
const logger = createLogger("kubernetesStatusService");
@@ -27,8 +27,10 @@ export default async function handler(req, res) {
}
const coreApi = kc.makeApiClient(CoreV1Api);
const podsResponse = await coreApi
- .listNamespacedPod(namespace, null, null, null, null, labelSelector)
- .then((response) => response.body)
+ .listNamespacedPod({
+ namespace,
+ labelSelector,
+ })
.catch((err) => {
logger.error("Error getting pods: %d %s %s", err.statusCode, err.body, err.response);
return null;
diff --git a/src/pages/api/releases.js b/src/pages/api/releases.js
index 14d3051d..01d0e8a5 100644
--- a/src/pages/api/releases.js
+++ b/src/pages/api/releases.js
@@ -1,6 +1,14 @@
-import cachedFetch from "utils/proxy/cached-fetch";
+import createLogger from "utils/logger";
+import { cachedRequest } from "utils/proxy/http";
+
+const logger = createLogger("releases");
export default async function handler(req, res) {
const releasesURL = "https://api.github.com/repos/gethomepage/homepage/releases";
- return res.send(await cachedFetch(releasesURL, 5));
+ try {
+ return res.send(await cachedRequest(releasesURL, 5));
+ } catch (e) {
+ logger.error(`Error checking GitHub releases: ${e}`);
+ return res.send([]);
+ }
}
diff --git a/src/pages/api/search/searchSuggestion.js b/src/pages/api/search/searchSuggestion.js
index fa8eba0d..13f3f301 100644
--- a/src/pages/api/search/searchSuggestion.js
+++ b/src/pages/api/search/searchSuggestion.js
@@ -1,6 +1,8 @@
import { searchProviders } from "components/widgets/search/search";
-import cachedFetch from "utils/proxy/cached-fetch";
+
+import { getSettings } from "utils/config/config";
import { widgetsFromConfig } from "utils/config/widget-helpers";
+import { cachedRequest } from "utils/proxy/http";
export default async function handler(req, res) {
const { query, providerName } = req.query;
@@ -11,13 +13,21 @@ export default async function handler(req, res) {
const widgets = await widgetsFromConfig();
const searchWidget = widgets.find((w) => w.type === "search");
- provider.url = searchWidget.options.url;
- provider.suggestionUrl = searchWidget.options.suggestionUrl;
+ if (searchWidget) {
+ provider.url = searchWidget.options.url;
+ provider.suggestionUrl = searchWidget.options.suggestionUrl;
+ } else {
+ const settings = getSettings();
+ if (settings.quicklaunch && settings.quicklaunch.provider === "custom") {
+ provider.url = settings.quicklaunch.url;
+ provider.suggestionUrl = settings.quicklaunch.suggestionUrl;
+ }
+ }
}
if (!provider.suggestionUrl) {
return res.json([query, []]); // Responde with the same array format but with no suggestions.
}
- return res.send(await cachedFetch(`${provider.suggestionUrl}${encodeURIComponent(query)}`, 5, "Mozilla/5.0"));
+ return res.send(await cachedRequest(`${provider.suggestionUrl}${encodeURIComponent(query)}`, 5, "Mozilla/5.0"));
}
diff --git a/src/pages/api/services/index.js b/src/pages/api/services/index.js
index 46d0a721..f6ba54c8 100644
--- a/src/pages/api/services/index.js
+++ b/src/pages/api/services/index.js
@@ -1,5 +1,8 @@
+import { readIdentitySettings } from "utils/identity/identity-helpers";
import { servicesResponse } from "utils/config/api-response";
+import { getSettings } from "utils/config/config";
export default async function handler(req, res) {
- res.send(await servicesResponse());
+ const { provider, groups } = readIdentitySettings(getSettings().identity);
+ res.send(await servicesResponse(provider.getIdentity(req), groups));
}
diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js
index 3f8adc88..0cdf806f 100644
--- a/src/pages/api/services/proxy.js
+++ b/src/pages/api/services/proxy.js
@@ -1,9 +1,9 @@
-import { formatApiCall } from "utils/proxy/api-helpers";
-import createLogger from "utils/logger";
-import genericProxyHandler from "utils/proxy/handlers/generic";
-import widgets from "widgets/widgets";
-import calendarProxyHandler from "widgets/calendar/proxy";
import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import genericProxyHandler from "utils/proxy/handlers/generic";
+import calendarProxyHandler from "widgets/calendar/proxy";
+import widgets from "widgets/widgets";
const logger = createLogger("servicesProxy");
diff --git a/src/pages/api/widgets/glances.js b/src/pages/api/widgets/glances.js
index 199c133e..f0a3a7d9 100644
--- a/src/pages/api/widgets/glances.js
+++ b/src/pages/api/widgets/glances.js
@@ -1,6 +1,6 @@
-import { httpProxy } from "utils/proxy/http";
-import createLogger from "utils/logger";
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
+import createLogger from "utils/logger";
+import { httpProxy } from "utils/proxy/http";
const logger = createLogger("glances");
diff --git a/src/pages/api/widgets/index.js b/src/pages/api/widgets/index.js
index 513e02e9..447ef340 100644
--- a/src/pages/api/widgets/index.js
+++ b/src/pages/api/widgets/index.js
@@ -1,5 +1,8 @@
+import { readIdentitySettings } from "utils/identity/identity-helpers";
import { widgetsResponse } from "utils/config/api-response";
+import { getSettings } from "utils/config/config";
export default async function handler(req, res) {
- res.send(await widgetsResponse());
+ const { provider } = readIdentitySettings(getSettings().identity);
+ res.send(await widgetsResponse(provider.getIdentity(req)));
}
diff --git a/src/pages/api/widgets/kubernetes.js b/src/pages/api/widgets/kubernetes.js
index 778a6aa1..583f6f15 100644
--- a/src/pages/api/widgets/kubernetes.js
+++ b/src/pages/api/widgets/kubernetes.js
@@ -1,10 +1,10 @@
import { CoreV1Api, Metrics } from "@kubernetes/client-node";
-import getKubeConfig from "../../../utils/config/kubernetes";
-import { parseCpu, parseMemory } from "../../../utils/kubernetes/kubernetes-utils";
+import { getKubeConfig } from "../../../utils/config/kubernetes";
+import { parseCpu, parseMemory } from "../../../utils/kubernetes/utils";
import createLogger from "../../../utils/logger";
-const logger = createLogger("kubernetes-widget");
+const logger = createLogger("widget");
export default async function handler(req, res) {
try {
@@ -17,14 +17,11 @@ export default async function handler(req, res) {
const coreApi = kc.makeApiClient(CoreV1Api);
const metricsApi = new Metrics(kc);
- const nodes = await coreApi
- .listNode()
- .then((response) => response.body)
- .catch((error) => {
- logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
- logger.debug(error);
- return null;
- });
+ const nodes = await coreApi.listNode().catch((error) => {
+ logger.error("Error getting nodes: %d %s %s", error.statusCode, error.body, error.response);
+ logger.debug(error);
+ return null;
+ });
if (!nodes) {
return res.status(500).send({
error: "An error occurred while fetching nodes, check logs for more details.",
diff --git a/src/pages/api/widgets/longhorn.js b/src/pages/api/widgets/longhorn.js
index 9126e937..c45086bc 100644
--- a/src/pages/api/widgets/longhorn.js
+++ b/src/pages/api/widgets/longhorn.js
@@ -1,6 +1,6 @@
-import { httpProxy } from "../../../utils/proxy/http";
-import createLogger from "../../../utils/logger";
import { getSettings } from "../../../utils/config/config";
+import createLogger from "../../../utils/logger";
+import { httpProxy } from "../../../utils/proxy/http";
const logger = createLogger("longhorn");
diff --git a/src/pages/api/widgets/openmeteo.js b/src/pages/api/widgets/openmeteo.js
index e63847b4..28f2e4f0 100644
--- a/src/pages/api/widgets/openmeteo.js
+++ b/src/pages/api/widgets/openmeteo.js
@@ -1,9 +1,9 @@
-import cachedFetch from "utils/proxy/cached-fetch";
+import { cachedRequest } from "utils/proxy/http";
export default async function handler(req, res) {
const { latitude, longitude, units, cache, timezone } = req.query;
const degrees = units === "metric" ? "celsius" : "fahrenheit";
const timezeone = timezone ?? "auto";
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset¤t_weather=true&temperature_unit=${degrees}&timezone=${timezeone}`;
- return res.send(await cachedFetch(apiUrl, cache));
+ return res.send(await cachedRequest(apiUrl, cache));
}
diff --git a/src/pages/api/widgets/openweathermap.js b/src/pages/api/widgets/openweathermap.js
index 089ee804..993ee1f5 100644
--- a/src/pages/api/widgets/openweathermap.js
+++ b/src/pages/api/widgets/openweathermap.js
@@ -1,6 +1,6 @@
-import cachedFetch from "utils/proxy/cached-fetch";
import { getSettings } from "utils/config/config";
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
+import { cachedRequest } from "utils/proxy/http";
export default async function handler(req, res) {
const { latitude, longitude, units, provider, cache, lang, index } = req.query;
@@ -26,5 +26,5 @@ export default async function handler(req, res) {
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${apiKey}&units=${units}&lang=${lang}`;
- return res.send(await cachedFetch(apiUrl, cache));
+ return res.send(await cachedRequest(apiUrl, cache));
}
diff --git a/src/pages/api/widgets/stocks.js b/src/pages/api/widgets/stocks.js
index 3941a773..77881ad7 100644
--- a/src/pages/api/widgets/stocks.js
+++ b/src/pages/api/widgets/stocks.js
@@ -1,6 +1,6 @@
-import cachedFetch from "utils/proxy/cached-fetch";
import { getSettings } from "utils/config/config";
import createLogger from "utils/logger";
+import { cachedRequest } from "utils/proxy/http";
const logger = createLogger("stocks");
@@ -60,7 +60,7 @@ export default async function handler(req, res) {
const apiUrl = `https://finnhub.io/api/v1/quote?symbol=${ticker}&token=${apiKey}`;
// Finnhub free accounts allow up to 60 calls/minute
// https://finnhub.io/pricing
- const { c, dp } = await cachedFetch(apiUrl, cache || 1);
+ const { c, dp } = await cachedRequest(apiUrl, cache || 1);
logger.debug("Finnhub API response for %s: %o", ticker, { c, dp });
// API sometimes returns 200, but values returned are `null`
diff --git a/src/pages/api/widgets/weather.js b/src/pages/api/widgets/weather.js
index 9d0451ce..78418f74 100644
--- a/src/pages/api/widgets/weather.js
+++ b/src/pages/api/widgets/weather.js
@@ -1,6 +1,6 @@
-import cachedFetch from "utils/proxy/cached-fetch";
import { getSettings } from "utils/config/config";
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
+import { cachedRequest } from "utils/proxy/http";
export default async function handler(req, res) {
const { latitude, longitude, provider, cache, lang, index } = req.query;
@@ -26,5 +26,5 @@ export default async function handler(req, res) {
const apiUrl = `http://api.weatherapi.com/v1/current.json?q=${latitude},${longitude}&key=${apiKey}&lang=${lang}`;
- return res.send(await cachedFetch(apiUrl, cache));
+ return res.send(await cachedRequest(apiUrl, cache));
}
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index 7dbadbf1..95c19dd3 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -1,31 +1,33 @@
/* eslint-disable react/no-array-index-key */
-import useSWR, { SWRConfig } from "swr";
-import Head from "next/head";
-import Script from "next/script";
-import dynamic from "next/dynamic";
import classNames from "classnames";
-import { useTranslation } from "next-i18next";
-import { useEffect, useContext, useState, useMemo } from "react";
-import { BiError } from "react-icons/bi";
-import { serverSideTranslations } from "next-i18next/serverSideTranslations";
-import { useRouter } from "next/router";
-
-import Tab, { slugifyAndEncode } from "components/tab";
-import ServicesGroup from "components/services/group";
import BookmarksGroup from "components/bookmarks/group";
-import Widget from "components/widgets/widget";
+import ErrorBoundary from "components/errorboundry";
+import QuickLaunch from "components/quicklaunch";
+import ServicesGroup from "components/services/group";
+import Tab, { slugifyAndEncode } from "components/tab";
import Revalidate from "components/toggles/revalidate";
-import createLogger from "utils/logger";
-import useWindowFocus from "utils/hooks/window-focus";
-import { getSettings } from "utils/config/config";
+import Widget from "components/widgets/widget";
+import { useTranslation } from "next-i18next";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+import dynamic from "next/dynamic";
+import Head from "next/head";
+import { useRouter } from "next/router";
+import Script from "next/script";
+import { useContext, useEffect, useMemo, useState } from "react";
+import { BiError } from "react-icons/bi";
+import useSWR, { SWRConfig, unstable_serialize as unstableSerialize } from "swr";
import { ColorContext } from "utils/contexts/color";
-import { ThemeContext } from "utils/contexts/theme";
import { SettingsContext } from "utils/contexts/settings";
import { TabContext } from "utils/contexts/tab";
+import { ThemeContext } from "utils/contexts/theme";
+
+import { fetchWithIdentity, readIdentitySettings } from "utils/identity/identity-helpers";
+import NullIdentityProvider from "utils/identity/null";
import { bookmarksResponse, servicesResponse, widgetsResponse } from "utils/config/api-response";
-import ErrorBoundary from "components/errorboundry";
+import { getSettings } from "utils/config/config";
+import useWindowFocus from "utils/hooks/window-focus";
+import createLogger from "utils/logger";
import themes from "utils/styles/themes";
-import QuickLaunch from "components/quicklaunch";
const ThemeToggle = dynamic(() => import("components/toggles/theme"), {
ssr: false,
@@ -41,25 +43,28 @@ const Version = dynamic(() => import("components/version"), {
const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "openmeteo", "search", "datetime"];
-export async function getStaticProps() {
+export async function getServerSideProps({ req }) {
let logger;
try {
logger = createLogger("index");
- const { providers, ...settings } = getSettings();
+ const { providers, identity, ...settings } = getSettings();
+ const { provider, groups } = readIdentitySettings(identity);
- const services = await servicesResponse();
- const bookmarks = await bookmarksResponse();
- const widgets = await widgetsResponse();
+ const services = await servicesResponse(provider.getIdentity(req), groups);
+ const bookmarks = await bookmarksResponse(provider.getIdentity(req), groups);
+ const widgets = await widgetsResponse(provider.getIdentity(req));
+ const identityContext = provider.getContext(req);
return {
props: {
initialSettings: settings,
fallback: {
- "/api/services": services,
- "/api/bookmarks": bookmarks,
- "/api/widgets": widgets,
+ [unstableSerialize(["/api/services", identityContext])]: services,
+ [unstableSerialize(["/api/bookmarks", identityContext])]: bookmarks,
+ [unstableSerialize(["/api/widgets", identityContext])]: widgets,
"/api/hash": false,
},
+ identityContext,
...(await serverSideTranslations(settings.language ?? "en")),
},
};
@@ -67,25 +72,28 @@ export async function getStaticProps() {
if (logger && e) {
logger.error(e);
}
+ const identityContext = NullIdentityProvider.create().getContext(req);
return {
props: {
initialSettings: {},
fallback: {
- "/api/services": [],
- "/api/bookmarks": [],
- "/api/widgets": [],
+ [unstableSerialize(["/api/services", identityContext])]: [],
+ [unstableSerialize(["/api/bookmarks", identityContext])]: [],
+ [unstableSerialize(["/api/widgets", identityContext])]: [],
"/api/hash": false,
},
+ identityContext,
...(await serverSideTranslations("en")),
},
};
}
}
-function Index({ initialSettings, fallback }) {
+function Index({ initialSettings, fallback, identityContext }) {
const windowFocused = useWindowFocus();
const [stale, setStale] = useState(false);
const { data: errorsData } = useSWR("/api/validate");
+ const { error: validateError } = errorsData || {};
const { data: hashData, mutate: mutateHash } = useSWR("/api/hash");
useEffect(() => {
@@ -117,6 +125,24 @@ function Index({ initialSettings, fallback }) {
}
}, [hashData]);
+ if (validateError) {
+ return (
+
+ );
+ }
+
if (stale) {
return (
@@ -152,7 +178,7 @@ function Index({ initialSettings, fallback }) {
return (
fetch(resource, init).then((res) => res.json()) }}>
-
+
);
@@ -166,7 +192,19 @@ const headerStyles = {
boxedWidgets: "m-5 mb-0 sm:m-9 sm:mb-0 sm:mt-1",
};
-function Home({ initialSettings }) {
+function getAllServices(services) {
+ function getServices(group) {
+ let nestedServices = [...group.services];
+ if (group.groups.length > 0) {
+ nestedServices = [...nestedServices, ...group.groups.map(getServices).flat()];
+ }
+ return nestedServices;
+ }
+
+ return [...services.map(getServices).flat()];
+}
+
+function Home({ initialSettings, identityContext }) {
const { i18n } = useTranslation();
const { theme, setTheme } = useContext(ThemeContext);
const { color, setColor } = useContext(ColorContext);
@@ -178,14 +216,13 @@ function Home({ initialSettings }) {
setSettings(initialSettings);
}, [initialSettings, setSettings]);
- const { data: services } = useSWR("/api/services");
- const { data: bookmarks } = useSWR("/api/bookmarks");
- const { data: widgets } = useSWR("/api/widgets");
+ const { data: services } = useSWR(["/api/services", identityContext], fetchWithIdentity);
+ const { data: bookmarks } = useSWR(["/api/bookmarks", identityContext], fetchWithIdentity);
+ const { data: widgets } = useSWR(["/api/widgets", identityContext], fetchWithIdentity);
- const servicesAndBookmarks = [
- ...services.map((sg) => sg.services).flat(),
- ...bookmarks.map((bg) => bg.bookmarks).flat(),
- ].filter((i) => i?.href);
+ const servicesAndBookmarks = [...bookmarks.map((bg) => bg.bookmarks).flat(), ...getAllServices(services)].filter(
+ (i) => i?.href,
+ );
useEffect(() => {
if (settings.language) {
@@ -293,7 +330,7 @@ function Home({ initialSettings }) {
key={group.name}
group={group}
layout={settings.layout?.[group.name]}
- fiveColumns={settings.fiveColumns}
+ maxGroupColumns={settings.fiveColumns ? 5 : settings.maxGroupColumns}
disableCollapse={settings.disableCollapse}
useEqualHeights={settings.useEqualHeights}
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
@@ -304,6 +341,7 @@ function Home({ initialSettings }) {
bookmarks={group}
layout={settings.layout?.[group.name]}
disableCollapse={settings.disableCollapse}
+ maxGroupColumns={settings.maxBookmarkGroupColumns ?? settings.maxGroupColumns}
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
/>
),
@@ -317,7 +355,7 @@ function Home({ initialSettings }) {
key={group.name}
group={group}
layout={settings.layout?.[group.name]}
- fiveColumns={settings.fiveColumns}
+ maxGroupColumns={settings.fiveColumns ? 5 : settings.maxGroupColumns}
disableCollapse={settings.disableCollapse}
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
/>
@@ -332,6 +370,7 @@ function Home({ initialSettings }) {
bookmarks={group}
layout={settings.layout?.[group.name]}
disableCollapse={settings.disableCollapse}
+ maxGroupColumns={settings.maxBookmarkGroupColumns ?? settings.maxGroupColumns}
groupsInitiallyCollapsed={settings.groupsInitiallyCollapsed}
bookmarksStyle={settings.bookmarksStyle}
/>
@@ -347,6 +386,8 @@ function Home({ initialSettings }) {
bookmarks,
settings.layout,
settings.fiveColumns,
+ settings.maxGroupColumns,
+ settings.maxBookmarkGroupColumns,
settings.disableCollapse,
settings.useEqualHeights,
settings.cardBlur,
@@ -387,7 +428,12 @@ function Home({ initialSettings }) {
-
+
- {!settings.hideVersion && }
+ {!settings.hideVersion && }
@@ -458,8 +504,8 @@ function Home({ initialSettings }) {
);
}
-export default function Wrapper({ initialSettings, fallback }) {
- const { theme } = useContext(ThemeContext);
+export default function Wrapper({ initialSettings, fallback, identityContext }) {
+ const { themeContext } = useContext(ThemeContext);
const wrappedStyle = {};
let backgroundBlur = false;
let backgroundSaturate = false;
@@ -490,9 +536,9 @@ export default function Wrapper({ initialSettings, fallback }) {
id="page_wrapper"
className={classNames(
"relative",
- theme && theme,
+ initialSettings.theme && initialSettings.theme,
initialSettings.color && `theme-${initialSettings.color}`,
- theme === "dark" ? "scheme-dark" : "scheme-light",
+ themeContext === "dark" ? "scheme-dark" : "scheme-light",
)}
>
-
+
diff --git a/src/styles/globals.css b/src/styles/globals.css
index b1cc1edb..27d6c2ff 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -1,6 +1,28 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
+@import 'tailwindcss';
+
+@config '../../tailwind.config.js';
+
+@theme {
+ --breakpoint-3xl: 112rem;
+}
+
+/*
+ The default border color has changed to `currentColor` in Tailwind CSS v4,
+ so we've added these compatibility styles to make sure everything still
+ looks the same as it did with Tailwind CSS v3.
+
+ If we ever want to remove these styles, we need to add an explicit border
+ color utility to any element that depends on these defaults.
+*/
+@layer base {
+ *,
+ ::after,
+ ::before,
+ ::backdrop,
+ ::file-selector-button {
+ border-color: var(--color-gray-200, currentColor);
+ }
+}
#__next {
width: 100%;
@@ -67,3 +89,8 @@ dialog ::-webkit-scrollbar {
.chart + .chart {
margin-top: 2em;
}
+
+.service-container + .chart {
+ margin-top: 2.5rem;
+ margin-bottom: .5rem;
+}
diff --git a/src/utils/config/api-response.js b/src/utils/config/api-response.js
index 37c80055..4ceb525f 100644
--- a/src/utils/config/api-response.js
+++ b/src/utils/config/api-response.js
@@ -4,15 +4,16 @@ import path from "path";
import yaml from "js-yaml";
-import checkAndCopyConfig, { getSettings, substituteEnvironmentVars, CONF_DIR } from "utils/config/config";
+import checkAndCopyConfig, { CONF_DIR, getSettings, substituteEnvironmentVars } from "utils/config/config";
import {
+ cleanServiceGroups,
+ findGroupByName,
servicesFromConfig,
servicesFromDocker,
- cleanServiceGroups,
servicesFromKubernetes,
- findGroupByName,
} from "utils/config/service-helpers";
import { cleanWidgetGroups, widgetsFromConfig } from "utils/config/widget-helpers";
+import { filterAllowedBookmarks, filterAllowedServices, filterAllowedWidgets } from "utils/identity/identity-helpers";
/**
* Compares services by weight then by name.
@@ -25,7 +26,7 @@ function compareServices(service1, service2) {
return service1.name.localeCompare(service2.name);
}
-export async function bookmarksResponse() {
+export async function bookmarksResponse(perms, idGroups) {
checkAndCopyConfig("bookmarks.yaml");
const bookmarksYaml = path.join(CONF_DIR, "bookmarks.yaml");
@@ -46,13 +47,17 @@ export async function bookmarksResponse() {
}
// map easy to write YAML objects into easy to consume JS arrays
- const bookmarksArray = bookmarks.map((group) => ({
- name: Object.keys(group)[0],
- bookmarks: group[Object.keys(group)[0]].map((entries) => ({
- name: Object.keys(entries)[0],
- ...entries[Object.keys(entries)[0]][0],
+ const bookmarksArray = filterAllowedBookmarks(
+ perms,
+ idGroups,
+ bookmarks.map((group) => ({
+ name: Object.keys(group)[0],
+ bookmarks: group[Object.keys(group)[0]].map((entries) => ({
+ name: Object.keys(entries)[0],
+ ...entries[Object.keys(entries)[0]][0],
+ })),
})),
- }));
+ );
const sortedGroups = [];
const unsortedGroups = [];
@@ -71,11 +76,11 @@ export async function bookmarksResponse() {
return [...sortedGroups.filter((g) => g), ...unsortedGroups];
}
-export async function widgetsResponse() {
+export async function widgetsResponse(perms) {
let configuredWidgets;
try {
- configuredWidgets = cleanWidgetGroups(await widgetsFromConfig());
+ configuredWidgets = filterAllowedWidgets(perms, await cleanWidgetGroups(await widgetsFromConfig()));
} catch (e) {
console.error("Failed to load widgets, please check widgets.yaml for errors or remove example entries.");
if (e) console.error(e);
@@ -85,6 +90,18 @@ export async function widgetsResponse() {
return configuredWidgets;
}
+function convertLayoutGroupToGroup(name, layoutGroup) {
+ const group = { name, services: [], groups: [] };
+ if (layoutGroup) {
+ Object.entries(layoutGroup).forEach(([key, value]) => {
+ if (typeof value === "object") {
+ group.groups.push(convertLayoutGroupToGroup(key, value));
+ }
+ });
+ }
+ return group;
+}
+
function mergeSubgroups(configuredGroups, mergedGroup) {
configuredGroups.forEach((group) => {
if (group.name === mergedGroup.name) {
@@ -96,14 +113,41 @@ function mergeSubgroups(configuredGroups, mergedGroup) {
});
}
-export async function servicesResponse() {
+function ensureParentGroupExists(sortedGroups, configuredGroups, group, definedLayouts) {
+ // make sure the top level parent group exists in the sortedGroups array
+ const parentGroupName = group.parent;
+ const parentGroup = findGroupByName(configuredGroups, parentGroupName);
+ if (parentGroup && parentGroup.parent) {
+ ensureParentGroupExists(sortedGroups, configuredGroups, parentGroup);
+ } else {
+ const parentGroupIndex = definedLayouts.findIndex((layout) => layout === parentGroupName);
+ if (parentGroupIndex > -1) {
+ sortedGroups[parentGroupIndex] = parentGroup;
+ }
+ }
+}
+
+function pruneEmptyGroups(groups) {
+ // remove any groups that have no services
+ return groups.filter((group) => {
+ if (group.services.length === 0 && group.groups.length === 0) {
+ return false;
+ }
+ if (group.groups.length > 0) {
+ group.groups = pruneEmptyGroups(group.groups);
+ }
+ return true;
+ });
+}
+
+export async function servicesResponse(perms, idGroups) {
let discoveredDockerServices;
let discoveredKubernetesServices;
let configuredServices;
let initialSettings;
try {
- discoveredDockerServices = cleanServiceGroups(await servicesFromDocker());
+ discoveredDockerServices = filterAllowedServices(perms, idGroups, cleanServiceGroups(await servicesFromDocker()));
if (discoveredDockerServices?.length === 0) {
console.debug("No containers were found with homepage labels.");
}
@@ -114,7 +158,11 @@ export async function servicesResponse() {
}
try {
- discoveredKubernetesServices = cleanServiceGroups(await servicesFromKubernetes());
+ discoveredKubernetesServices = filterAllowedServices(
+ perms,
+ idGroups,
+ cleanServiceGroups(await servicesFromKubernetes()),
+ );
} catch (e) {
console.error("Failed to discover services, please check kubernetes.yaml for errors or remove example entries.");
if (e) console.error(e.toString());
@@ -122,7 +170,7 @@ export async function servicesResponse() {
}
try {
- configuredServices = cleanServiceGroups(await servicesFromConfig());
+ configuredServices = filterAllowedServices(perms, idGroups, cleanServiceGroups(await servicesFromConfig()));
} catch (e) {
console.error("Failed to load services.yaml, please check for errors");
if (e) console.error(e.toString());
@@ -150,6 +198,17 @@ export async function servicesResponse() {
const sortedGroups = [];
const unsortedGroups = [];
const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null;
+ if (definedLayouts) {
+ // this handles cases where groups are only defined in the settings.yaml layout and not in the services.yaml
+ const layoutConfiguredGroups = Object.entries(initialSettings.layout).map(([key, value]) =>
+ convertLayoutGroupToGroup(key, value),
+ );
+ layoutConfiguredGroups.forEach((group) => {
+ if (!configuredServices.find((serviceGroup) => serviceGroup.name === group.name)) {
+ configuredServices.push(group);
+ }
+ });
+ }
mergedGroupsNames.forEach((groupName) => {
const discoveredDockerGroup = findGroupByName(discoveredDockerServices, groupName) || {
@@ -174,11 +233,17 @@ export async function servicesResponse() {
else if (configuredGroup.parent) {
// this is a nested group, so find the parent group and merge the services
mergeSubgroups(configuredServices, mergedGroup);
+ // make sure the top level parent group exists in the sortedGroups array
+ ensureParentGroupExists(sortedGroups, configuredServices, configuredGroup, definedLayouts);
} else unsortedGroups.push(mergedGroup);
+ } else if (configuredGroup.parent) {
+ // this is a nested group, so find the parent group and merge the services
+ mergeSubgroups(configuredServices, mergedGroup);
} else {
unsortedGroups.push(mergedGroup);
}
});
- return [...sortedGroups.filter((g) => g), ...unsortedGroups];
+ const allGroups = [...sortedGroups.filter((g) => g), ...unsortedGroups];
+ return pruneEmptyGroups(allGroups);
}
diff --git a/src/utils/config/config.js b/src/utils/config/config.js
index 18dedf62..3f688842 100644
--- a/src/utils/config/config.js
+++ b/src/utils/config/config.js
@@ -1,9 +1,9 @@
/* eslint-disable no-console */
-import { join } from "path";
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
+import { join } from "path";
-import cache from "memory-cache";
import yaml from "js-yaml";
+import cache from "memory-cache";
const cacheKey = "homepageEnvironmentVariables";
const homepageVarPrefix = "HOMEPAGE_VAR_";
diff --git a/src/utils/config/docker.js b/src/utils/config/docker.js
index 6ea728e9..ed1d3347 100644
--- a/src/utils/config/docker.js
+++ b/src/utils/config/docker.js
@@ -1,5 +1,5 @@
-import path from "path";
import { readFileSync } from "fs";
+import path from "path";
import yaml from "js-yaml";
diff --git a/src/utils/config/kubernetes.js b/src/utils/config/kubernetes.js
index 6693a98d..680c408e 100644
--- a/src/utils/config/kubernetes.js
+++ b/src/utils/config/kubernetes.js
@@ -1,19 +1,22 @@
-import path from "path";
import { readFileSync } from "fs";
+import path from "path";
+import { ApiextensionsV1Api, KubeConfig } from "@kubernetes/client-node";
import yaml from "js-yaml";
-import { KubeConfig } from "@kubernetes/client-node";
import checkAndCopyConfig, { CONF_DIR, substituteEnvironmentVars } from "utils/config/config";
-export default function getKubeConfig() {
+export function getKubernetes() {
checkAndCopyConfig("kubernetes.yaml");
-
const configFile = path.join(CONF_DIR, "kubernetes.yaml");
const rawConfigData = readFileSync(configFile, "utf8");
const configData = substituteEnvironmentVars(rawConfigData);
- const config = yaml.load(configData);
+ return yaml.load(configData);
+}
+
+export const getKubeConfig = () => {
const kc = new KubeConfig();
+ const config = getKubernetes();
switch (config?.mode) {
case "cluster":
@@ -28,4 +31,31 @@ export default function getKubeConfig() {
}
return kc;
+};
+
+export async function checkCRD(name, kc, logger) {
+ const apiExtensions = kc.makeApiClient(ApiextensionsV1Api);
+ const exist = await apiExtensions
+ .readCustomResourceDefinitionStatus({
+ name,
+ })
+ .then(() => true)
+ .catch(async (error) => {
+ if (error.statusCode === 403) {
+ logger.error(
+ "Error checking if CRD %s exists. Make sure to add the following permission to your RBAC: %d %s %s",
+ name,
+ error.statusCode,
+ error.body.message,
+ );
+ }
+ return false;
+ });
+
+ return exist;
}
+
+export const ANNOTATION_BASE = "gethomepage.dev";
+export const ANNOTATION_WIDGET_BASE = `${ANNOTATION_BASE}/widget.`;
+export const HTTPROUTE_API_GROUP = "gateway.networking.k8s.io";
+export const HTTPROUTE_API_VERSION = "v1";
diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js
index 53fe47cc..297e55a7 100644
--- a/src/utils/config/service-helpers.js
+++ b/src/utils/config/service-helpers.js
@@ -1,15 +1,15 @@
import { promises as fs } from "fs";
import path from "path";
-import yaml from "js-yaml";
import Docker from "dockerode";
-import { CustomObjectsApi, NetworkingV1Api, ApiextensionsV1Api } from "@kubernetes/client-node";
+import yaml from "js-yaml";
-import createLogger from "utils/logger";
import checkAndCopyConfig, { CONF_DIR, getSettings, substituteEnvironmentVars } from "utils/config/config";
import getDockerArguments from "utils/config/docker";
-import getKubeConfig from "utils/config/kubernetes";
+import { getKubeConfig } from "utils/config/kubernetes";
import * as shvl from "utils/config/shvl";
+import kubernetes from "utils/kubernetes/export";
+import createLogger from "utils/logger";
const logger = createLogger("service-helpers");
@@ -167,36 +167,7 @@ export async function servicesFromDocker() {
return mappedServiceGroups;
}
-function getUrlFromIngress(ingress) {
- const urlHost = ingress.spec.rules[0].host;
- const urlPath = ingress.spec.rules[0].http.paths[0].path;
- const urlSchema = ingress.spec.tls ? "https" : "http";
- return `${urlSchema}://${urlHost}${urlPath}`;
-}
-
-export async function checkCRD(kc, name) {
- const apiExtensions = kc.makeApiClient(ApiextensionsV1Api);
- const exist = await apiExtensions
- .readCustomResourceDefinitionStatus(name)
- .then(() => true)
- .catch(async (error) => {
- if (error.statusCode === 403) {
- logger.error(
- "Error checking if CRD %s exists. Make sure to add the following permission to your RBAC: %d %s %s",
- name,
- error.statusCode,
- error.body.message,
- );
- }
- return false;
- });
-
- return exist;
-}
-
export async function servicesFromKubernetes() {
- const ANNOTATION_BASE = "gethomepage.dev";
- const ANNOTATION_WIDGET_BASE = `${ANNOTATION_BASE}/widget.`;
const { instanceName } = getSettings();
checkAndCopyConfig("kubernetes.yaml");
@@ -206,145 +177,46 @@ export async function servicesFromKubernetes() {
if (!kc) {
return [];
}
- const networking = kc.makeApiClient(NetworkingV1Api);
- const crd = kc.makeApiClient(CustomObjectsApi);
- const ingressList = await networking
- .listIngressForAllNamespaces(null, null, null, null)
- .then((response) => response.body)
- .catch((error) => {
- logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
- logger.debug(error);
- return null;
- });
+ // resource lists
+ const [ingressList, traefikIngressList, httpRouteList] = await Promise.all([
+ kubernetes.listIngress(),
+ kubernetes.listTraefikIngress(),
+ kubernetes.listHttpRoute(),
+ ]);
- const traefikContainoExists = await checkCRD(kc, "ingressroutes.traefik.containo.us");
- const traefikExists = await checkCRD(kc, "ingressroutes.traefik.io");
+ const resources = [...ingressList, ...traefikIngressList, ...httpRouteList];
- const traefikIngressListContaino = await crd
- .listClusterCustomObject("traefik.containo.us", "v1alpha1", "ingressroutes")
- .then((response) => response.body)
- .catch(async (error) => {
- if (traefikContainoExists) {
- logger.error(
- "Error getting traefik ingresses from traefik.containo.us: %d %s %s",
- error.statusCode,
- error.body,
- error.response,
- );
- logger.debug(error);
- }
-
- return [];
- });
-
- const traefikIngressListIo = await crd
- .listClusterCustomObject("traefik.io", "v1alpha1", "ingressroutes")
- .then((response) => response.body)
- .catch(async (error) => {
- if (traefikExists) {
- logger.error(
- "Error getting traefik ingresses from traefik.io: %d %s %s",
- error.statusCode,
- error.body,
- error.response,
- );
- logger.debug(error);
- }
-
- return [];
- });
-
- const traefikIngressList = [...(traefikIngressListContaino?.items ?? []), ...(traefikIngressListIo?.items ?? [])];
-
- if (traefikIngressList.length > 0) {
- const traefikServices = traefikIngressList.filter(
- (ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`],
- );
- ingressList.items.push(...traefikServices);
- }
-
- if (!ingressList) {
+ if (!resources) {
return [];
}
- const services = ingressList.items
- .filter(
- (ingress) =>
- ingress.metadata.annotations &&
- ingress.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === "true" &&
- (!ingress.metadata.annotations[`${ANNOTATION_BASE}/instance`] ||
- ingress.metadata.annotations[`${ANNOTATION_BASE}/instance`] === instanceName ||
- `${ANNOTATION_BASE}/instance.${instanceName}` in ingress.metadata.annotations),
- )
- .map((ingress) => {
- let constructedService = {
- app: ingress.metadata.annotations[`${ANNOTATION_BASE}/app`] || ingress.metadata.name,
- namespace: ingress.metadata.namespace,
- href: ingress.metadata.annotations[`${ANNOTATION_BASE}/href`] || getUrlFromIngress(ingress),
- name: ingress.metadata.annotations[`${ANNOTATION_BASE}/name`] || ingress.metadata.name,
- group: ingress.metadata.annotations[`${ANNOTATION_BASE}/group`] || "Kubernetes",
- weight: ingress.metadata.annotations[`${ANNOTATION_BASE}/weight`] || "0",
- icon: ingress.metadata.annotations[`${ANNOTATION_BASE}/icon`] || "",
- description: ingress.metadata.annotations[`${ANNOTATION_BASE}/description`] || "",
- external: false,
- type: "service",
- };
- if (ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]) {
- constructedService.external =
- String(ingress.metadata.annotations[`${ANNOTATION_BASE}/external`]).toLowerCase() === "true";
- }
- if (ingress.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`] !== undefined) {
- constructedService.podSelector = ingress.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`];
- }
- if (ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`]) {
- constructedService.ping = ingress.metadata.annotations[`${ANNOTATION_BASE}/ping`];
- }
- if (ingress.metadata.annotations[`${ANNOTATION_BASE}/siteMonitor`]) {
- constructedService.siteMonitor = ingress.metadata.annotations[`${ANNOTATION_BASE}/siteMonitor`];
- }
- if (ingress.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`]) {
- constructedService.statusStyle = ingress.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`];
- }
- Object.keys(ingress.metadata.annotations).forEach((annotation) => {
- if (annotation.startsWith(ANNOTATION_WIDGET_BASE)) {
- shvl.set(
- constructedService,
- annotation.replace(`${ANNOTATION_BASE}/`, ""),
- ingress.metadata.annotations[annotation],
- );
- }
- });
+ const services = await Promise.all(
+ resources
+ .filter((resource) => kubernetes.isDiscoverable(resource, instanceName))
+ .map(async (resource) => kubernetes.constructedServiceFromResource(resource)),
+ );
- try {
- constructedService = JSON.parse(substituteEnvironmentVars(JSON.stringify(constructedService)));
- } catch (e) {
- logger.error("Error attempting k8s environment variable substitution.");
- logger.debug(e);
- }
+ // map service groups
+ const mappedServiceGroups = services.reduce((groups, serverService) => {
+ let serverGroup = groups.find((group) => group.name === serverService.group);
- return constructedService;
- });
-
- const mappedServiceGroups = [];
-
- services.forEach((serverService) => {
- let serverGroup = mappedServiceGroups.find((searchedGroup) => searchedGroup.name === serverService.group);
if (!serverGroup) {
- mappedServiceGroups.push({
+ serverGroup = {
name: serverService.group,
services: [],
- });
- serverGroup = mappedServiceGroups[mappedServiceGroups.length - 1];
+ };
+ groups.push(serverGroup);
}
- const { name: serviceName, group: serverServiceGroup, ...pushedService } = serverService;
- const result = {
+ const { name: serviceName, group: _, ...pushedService } = serverService;
+
+ serverGroup.services.push({
name: serviceName,
...pushedService,
- };
+ });
- serverGroup.services.push(result);
- });
+ return groups;
+ }, []);
return mappedServiceGroups;
} catch (e) {
@@ -432,7 +304,7 @@ export function cleanServiceGroups(groups) {
// frigate
enableRecentEvents,
- // beszel, glances, immich, mealie, pihole, pfsense
+ // beszel, glances, immich, komga, mealie, pihole, pfsense, speedtest
version,
// glances
@@ -490,6 +362,9 @@ export function cleanServiceGroups(groups) {
// proxmox
node,
+ // proxmoxbackupserver
+ datastore,
+
// speedtest
bitratePrecision,
@@ -565,6 +440,9 @@ export function cleanServiceGroups(groups) {
if (type === "proxmox") {
if (node) widget.node = node;
}
+ if (type === "proxmoxbackupserver") {
+ if (datastore) widget.datastore = datastore;
+ }
if (type === "kubernetes") {
if (namespace) widget.namespace = namespace;
if (app) widget.app = app;
@@ -610,7 +488,7 @@ export function cleanServiceGroups(groups) {
if (snapshotHost) widget.snapshotHost = snapshotHost;
if (snapshotPath) widget.snapshotPath = snapshotPath;
}
- if (["beszel", "glances", "immich", "mealie", "pfsense", "pihole"].includes(type)) {
+ if (["beszel", "glances", "immich", "komga", "mealie", "pfsense", "pihole", "speedtest"].includes(type)) {
if (version) widget.version = parseInt(version, 10);
}
if (type === "glances") {
@@ -703,7 +581,7 @@ export function findGroupByName(groups, name) {
} else if (group.groups) {
const foundGroup = findGroupByName(group.groups, name);
if (foundGroup) {
- foundGroup.parent = group;
+ foundGroup.parent = group.name;
return foundGroup;
}
}
diff --git a/src/utils/config/widget-helpers.js b/src/utils/config/widget-helpers.js
index 7c5c78cd..93f71194 100644
--- a/src/utils/config/widget-helpers.js
+++ b/src/utils/config/widget-helpers.js
@@ -38,7 +38,7 @@ export async function cleanWidgetGroups(widgets) {
}
});
- // delete url from the sanitized options if the widget is not a search or glances widgeth
+ // delete url from the sanitized options if the widget is not a search or glances widget
if (widget.type !== "search" && widget.type !== "glances" && optionKeys.includes("url")) {
delete sanitizedOptions.url;
}
@@ -56,21 +56,22 @@ export async function cleanWidgetGroups(widgets) {
export async function getPrivateWidgetOptions(type, widgetIndex) {
const widgets = await widgetsFromConfig();
- const privateOptions = widgets.map((widget) => {
- const { index, url, username, password, key, apiKey } = widget.options;
+ const privateOptions =
+ widgets.map((widget) => {
+ const { index, url, username, password, key, apiKey } = widget.options;
- return {
- type: widget.type,
- options: {
- index,
- url,
- username,
- password,
- key,
- apiKey,
- },
- };
- });
+ return {
+ type: widget.type,
+ options: {
+ index,
+ url,
+ username,
+ password,
+ key,
+ apiKey,
+ },
+ };
+ }) || {};
return type !== undefined && widgetIndex !== undefined
? privateOptions.find((o) => o.type === type && o.options.index === parseInt(widgetIndex, 10))?.options
diff --git a/src/utils/contexts/color.jsx b/src/utils/contexts/color.jsx
index d7d985f0..bc16d605 100644
--- a/src/utils/contexts/color.jsx
+++ b/src/utils/contexts/color.jsx
@@ -1,4 +1,4 @@
-import { createContext, useState, useEffect, useMemo } from "react";
+import { createContext, useEffect, useMemo, useState } from "react";
let lastColor = false;
diff --git a/src/utils/contexts/settings.jsx b/src/utils/contexts/settings.jsx
index d6993b14..76451953 100644
--- a/src/utils/contexts/settings.jsx
+++ b/src/utils/contexts/settings.jsx
@@ -1,4 +1,4 @@
-import { createContext, useState, useMemo } from "react";
+import { createContext, useMemo, useState } from "react";
export const SettingsContext = createContext();
diff --git a/src/utils/contexts/tab.jsx b/src/utils/contexts/tab.jsx
index 8cd5d520..2a3d3457 100644
--- a/src/utils/contexts/tab.jsx
+++ b/src/utils/contexts/tab.jsx
@@ -1,4 +1,4 @@
-import { createContext, useState, useMemo } from "react";
+import { createContext, useMemo, useState } from "react";
export const TabContext = createContext();
diff --git a/src/utils/contexts/theme.jsx b/src/utils/contexts/theme.jsx
index 85d613fc..385eeaa2 100644
--- a/src/utils/contexts/theme.jsx
+++ b/src/utils/contexts/theme.jsx
@@ -1,4 +1,4 @@
-import { createContext, useState, useEffect, useMemo } from "react";
+import { createContext, useEffect, useMemo, useState } from "react";
const getInitialTheme = () => {
if (typeof window !== "undefined" && window.localStorage) {
diff --git a/src/utils/hooks/window-focus.js b/src/utils/hooks/window-focus.js
index 3ad57ad0..a221e48e 100644
--- a/src/utils/hooks/window-focus.js
+++ b/src/utils/hooks/window-focus.js
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useEffect, useState } from "react";
const hasFocus = () => typeof document !== "undefined" && document.hasFocus();
diff --git a/src/utils/identity/identity-helpers.js b/src/utils/identity/identity-helpers.js
new file mode 100644
index 00000000..e50f83c5
--- /dev/null
+++ b/src/utils/identity/identity-helpers.js
@@ -0,0 +1,70 @@
+import ProxyIdentityProvider from "./proxy";
+import NullIdentityProvider from "./null";
+
+const IdentityProviders = {
+ null: NullIdentityProvider,
+ proxy: ProxyIdentityProvider,
+};
+
+function getProviderByKey(key) {
+ return IdentityProviders[key] || NullIdentityProvider;
+}
+
+function identityAllow({ user, groups }, item) {
+ const groupAllow =
+ "allowGroups" in item && item.allowGroups && groups.some((group) => item.allowGroups.includes(group));
+ const userAllow = "allowUsers" in item && item.allowUsers && item.allowUsers.includes(user);
+ const allowAll = !("allowGroups" in item && item.allowGroups) && !("allowUsers" in item && item.allowUsers);
+
+ return userAllow || groupAllow || allowAll;
+}
+
+export function checkAllowedGroup(perms, idGroups, groupName) {
+ const testGroup = idGroups.find((group) => group.name === groupName);
+ return testGroup ? identityAllow(perms, testGroup) : true;
+}
+
+function filterAllowedItems(perms, idGroups, groups, groupKey) {
+ return groups
+ .filter((group) => checkAllowedGroup(perms, idGroups, group.name))
+ .map((group) => ({
+ name: group.name,
+ [groupKey]: group[groupKey].filter((item) => identityAllow(perms, item)),
+ }))
+ .filter((group) => group[groupKey].length);
+}
+
+export function readIdentitySettings({ provider, groups } = {}) {
+ let groupArray = [];
+ if (groups) {
+ if (Array.isArray(groups)) {
+ groupArray = groups.map((group) => ({
+ name: Object.keys(group)[0],
+ allowUsers: group.allowUsers,
+ allowGroups: group.allowGroups,
+ }));
+ } else {
+ groupArray = Object.keys(groups).map((group) => ({
+ name: group,
+ allowUsers: groups[group].allowUsers,
+ allowGroups: groups[group].allowGroups,
+ }));
+ }
+ }
+
+ return {
+ provider: provider ? getProviderByKey(provider.type).create(provider) : NullIdentityProvider.create(),
+ groups: groupArray,
+ };
+}
+
+export async function fetchWithIdentity(key, context) {
+ return getProviderByKey(context.provider).fetch([key, context]);
+}
+
+export const filterAllowedServices = (perms, idGroups, services) =>
+ filterAllowedItems(perms, idGroups, services, "services");
+export const filterAllowedBookmarks = (perms, idGroups, bookmarks) =>
+ filterAllowedItems(perms, idGroups, bookmarks, "bookmarks");
+export const filterAllowedWidgets = (perms, widgets) =>
+ widgets.filter((widget) => identityAllow(perms, widget.options));
diff --git a/src/utils/identity/null.js b/src/utils/identity/null.js
new file mode 100644
index 00000000..476946f1
--- /dev/null
+++ b/src/utils/identity/null.js
@@ -0,0 +1,21 @@
+const NullIdentity = { user: null, groups: [] };
+
+function createNullIdentity() {
+ return {
+ getIdentity: () => NullIdentity,
+ getContext: () => ({
+ provider: "null",
+ }),
+ };
+}
+
+async function fetchNullIdentity([key]) {
+ return fetch(key).then((res) => res.json());
+}
+
+const NullIdentityProvider = {
+ create: createNullIdentity,
+ fetch: fetchNullIdentity,
+};
+
+export default NullIdentityProvider;
diff --git a/src/utils/identity/proxy.js b/src/utils/identity/proxy.js
new file mode 100644
index 00000000..7159a658
--- /dev/null
+++ b/src/utils/identity/proxy.js
@@ -0,0 +1,34 @@
+// 'proxy' identity provider is meant to be used by a reverse proxy that injects permission headers into the origin
+// request. In this case we are relying on our proxy to authenitcate our users and validate their identity.
+function getProxyPermissions(userHeader, groupHeader, groupSeparator, request) {
+ const user =
+ userHeader && request.headers[userHeader.toLowerCase()] ? request.headers[userHeader.toLowerCase()] : null;
+ const groupsString =
+ groupHeader && request.headers[groupHeader.toLowerCase()] ? request.headers[groupHeader.toLowerCase()] : "";
+
+ return { user, groups: groupsString ? groupsString.split(groupSeparator ?? "|").map((v) => v.trim()) : [] };
+}
+
+function createProxyIdentity({ groupHeader, groupSeparator, userHeader }) {
+ return {
+ getContext: (request) => ({
+ provider: "proxy",
+ ...(userHeader &&
+ request.headers[userHeader] && { [userHeader.toLowerCase()]: request.headers[userHeader.toLowerCase()] }),
+ ...(groupHeader &&
+ request.headers[groupHeader] && { [groupHeader.toLowerCase()]: request.headers[groupHeader.toLowerCase()] }),
+ }),
+ getIdentity: (request) => getProxyPermissions(userHeader, groupHeader, groupSeparator, request),
+ };
+}
+
+async function fetchProxyIdentity([key, context]) {
+ return fetch(key, { headers: context.headers }).then((res) => res.json());
+}
+
+const ProxyIdentityProvider = {
+ create: createProxyIdentity,
+ fetch: fetchProxyIdentity,
+};
+
+export default ProxyIdentityProvider;
diff --git a/src/utils/kubernetes/export.js b/src/utils/kubernetes/export.js
new file mode 100644
index 00000000..ae53aaca
--- /dev/null
+++ b/src/utils/kubernetes/export.js
@@ -0,0 +1,14 @@
+import listHttpRoute from "utils/kubernetes/httproute-list";
+import listIngress from "utils/kubernetes/ingress-list";
+import { constructedServiceFromResource, isDiscoverable } from "utils/kubernetes/resource-helpers";
+import listTraefikIngress from "utils/kubernetes/traefik-list";
+
+const kubernetes = {
+ listIngress,
+ listTraefikIngress,
+ listHttpRoute,
+ isDiscoverable,
+ constructedServiceFromResource,
+};
+
+export default kubernetes;
diff --git a/src/utils/kubernetes/httproute-list.js b/src/utils/kubernetes/httproute-list.js
new file mode 100644
index 00000000..df147d16
--- /dev/null
+++ b/src/utils/kubernetes/httproute-list.js
@@ -0,0 +1,55 @@
+import { CoreV1Api, CustomObjectsApi } from "@kubernetes/client-node";
+
+import { getKubeConfig, getKubernetes, HTTPROUTE_API_GROUP, HTTPROUTE_API_VERSION } from "utils/config/kubernetes";
+import createLogger from "utils/logger";
+
+const logger = createLogger("httproute-list");
+const kc = getKubeConfig();
+
+export default async function listHttpRoute() {
+ const crd = kc.makeApiClient(CustomObjectsApi);
+ const core = kc.makeApiClient(CoreV1Api);
+ const { gateway } = getKubernetes();
+ let httpRouteList = [];
+
+ if (gateway) {
+ // httproutes
+ const getHttpRoutes = async (namespace) =>
+ crd
+ .listNamespacedCustomObject({
+ group: HTTPROUTE_API_GROUP,
+ version: HTTPROUTE_API_VERSION,
+ namespace,
+ plural: "httproutes",
+ })
+ .then((response) => {
+ return response.items;
+ })
+ .catch((error) => {
+ logger.error("Error getting httproutes: %d %s %s", error.statusCode, error.body, error.response);
+ logger.debug(error);
+ return null;
+ });
+ // namespaces
+ const namespaces = await core
+ .listNamespace()
+ .then((response) => response.items.map((ns) => ns.metadata.name))
+ .catch((error) => {
+ logger.error("Error getting namespaces: %d %s %s", error.statusCode, error.body, error.response);
+ logger.debug(error);
+ return null;
+ });
+
+ if (namespaces) {
+ const httpRouteListUnfiltered = await Promise.all(
+ namespaces.map(async (namespace) => {
+ const httpRoutes = await getHttpRoutes(namespace);
+ return httpRoutes;
+ }),
+ );
+
+ httpRouteList = httpRouteListUnfiltered.flat().filter((httpRoute) => httpRoute);
+ }
+ }
+ return httpRouteList;
+}
diff --git a/src/utils/kubernetes/ingress-list.js b/src/utils/kubernetes/ingress-list.js
new file mode 100644
index 00000000..1cd9ca95
--- /dev/null
+++ b/src/utils/kubernetes/ingress-list.js
@@ -0,0 +1,26 @@
+import { NetworkingV1Api } from "@kubernetes/client-node";
+
+import { getKubeConfig, getKubernetes } from "utils/config/kubernetes";
+import createLogger from "utils/logger";
+
+const logger = createLogger("ingress-list");
+const kc = getKubeConfig();
+
+export default async function listIngress() {
+ const networking = kc.makeApiClient(NetworkingV1Api);
+ const { ingress = true } = getKubernetes();
+ let ingressList = [];
+
+ if (ingress) {
+ const ingressData = await networking
+ .listIngressForAllNamespaces()
+ .then((response) => response)
+ .catch((error) => {
+ logger.error("Error getting ingresses: %d %s %s", error.statusCode, error.body, error.response);
+ logger.debug(error);
+ return null;
+ });
+ ingressList = ingressData.items;
+ }
+ return ingressList;
+}
diff --git a/src/utils/kubernetes/resource-helpers.js b/src/utils/kubernetes/resource-helpers.js
new file mode 100644
index 00000000..c7d69751
--- /dev/null
+++ b/src/utils/kubernetes/resource-helpers.js
@@ -0,0 +1,143 @@
+import { CustomObjectsApi } from "@kubernetes/client-node";
+
+import { substituteEnvironmentVars } from "utils/config/config";
+import {
+ ANNOTATION_BASE,
+ ANNOTATION_WIDGET_BASE,
+ getKubeConfig,
+ HTTPROUTE_API_GROUP,
+ HTTPROUTE_API_VERSION,
+} from "utils/config/kubernetes";
+import * as shvl from "utils/config/shvl";
+import createLogger from "utils/logger";
+
+const logger = createLogger("resource-helpers");
+const kc = getKubeConfig();
+
+const getSchemaFromGateway = async (parentRef) => {
+ const crd = kc.makeApiClient(CustomObjectsApi);
+ const schema = await crd
+ .getNamespacedCustomObject({
+ group: HTTPROUTE_API_GROUP,
+ version: HTTPROUTE_API_VERSION,
+ namespace: parentRef.namespace,
+ plural: "gateways",
+ name: parentRef.name,
+ })
+ .then((response) => {
+ const listener =
+ response.spec.listeners.find((l) => l.name === parentRef.sectionName) ?? response.spec.listeners[0];
+
+ return listener.protocol.toLowerCase();
+ })
+ .catch((error) => {
+ logger.error("Error getting gateways: %d %s %s", error.statusCode, error.body, error.response);
+ logger.debug(error);
+
+ return "http";
+ });
+
+ return schema;
+};
+
+async function getUrlFromHttpRoute(resource) {
+ let url = null;
+ const hasHostName = resource.spec?.hostnames;
+
+ if (hasHostName) {
+ if (resource.spec.rules[0].matches[0].path.type !== "RegularExpression") {
+ const urlHost = resource.spec.hostnames[0];
+ const urlPath = resource.spec.rules[0].matches[0].path.value;
+ const urlSchema = await getSchemaFromGateway(resource.spec.parentRefs[0]);
+ url = `${urlSchema}://${urlHost}${urlPath}`;
+ }
+ }
+
+ return url;
+}
+
+function getUrlFromIngress(resource) {
+ const urlHost = resource.spec.rules[0].host;
+ const urlPath = resource.spec.rules[0].http.paths[0].path;
+ const urlSchema = resource.spec.tls ? "https" : "http";
+
+ return `${urlSchema}://${urlHost}${urlPath}`;
+}
+
+async function getUrlSchema(resource) {
+ const isHttpRoute = resource.kind === "HTTPRoute";
+ let urlSchema;
+ if (isHttpRoute) {
+ urlSchema = getUrlFromHttpRoute(resource);
+ } else {
+ urlSchema = getUrlFromIngress(resource);
+ }
+
+ return urlSchema;
+}
+
+export function isDiscoverable(resource, instanceName) {
+ return (
+ resource.metadata.annotations &&
+ resource.metadata.annotations[`${ANNOTATION_BASE}/enabled`] === "true" &&
+ (!resource.metadata.annotations[`${ANNOTATION_BASE}/instance`] ||
+ resource.metadata.annotations[`${ANNOTATION_BASE}/instance`] === instanceName ||
+ `${ANNOTATION_BASE}/instance.${instanceName}` in resource.metadata.annotations)
+ );
+}
+
+export async function constructedServiceFromResource(resource) {
+ let constructedService = {
+ app: resource.metadata.annotations[`${ANNOTATION_BASE}/app`] || resource.metadata.name,
+ namespace: resource.metadata.namespace,
+ href: resource.metadata.annotations[`${ANNOTATION_BASE}/href`] || (await getUrlSchema(resource)),
+ name: resource.metadata.annotations[`${ANNOTATION_BASE}/name`] || resource.metadata.name,
+ group: resource.metadata.annotations[`${ANNOTATION_BASE}/group`] || "Kubernetes",
+ weight: resource.metadata.annotations[`${ANNOTATION_BASE}/weight`] || "0",
+ icon: resource.metadata.annotations[`${ANNOTATION_BASE}/icon`] || "",
+ description: resource.metadata.annotations[`${ANNOTATION_BASE}/description`] || "",
+ external: false,
+ type: "service",
+ };
+ if (resource.metadata.annotations[`${ANNOTATION_BASE}/external`]) {
+ constructedService.external =
+ String(resource.metadata.annotations[`${ANNOTATION_BASE}/external`]).toLowerCase() === "true";
+ }
+ if (resource.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`] !== undefined) {
+ constructedService.podSelector = resource.metadata.annotations[`${ANNOTATION_BASE}/pod-selector`];
+ }
+ if (resource.metadata.annotations[`${ANNOTATION_BASE}/ping`]) {
+ constructedService.ping = resource.metadata.annotations[`${ANNOTATION_BASE}/ping`];
+ }
+ if (resource.metadata.annotations[`${ANNOTATION_BASE}/siteMonitor`]) {
+ constructedService.siteMonitor = resource.metadata.annotations[`${ANNOTATION_BASE}/siteMonitor`];
+ }
+ if (resource.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`]) {
+ constructedService.statusStyle = resource.metadata.annotations[`${ANNOTATION_BASE}/statusStyle`];
+ }
+ if (resource.metadata.annotations[`${ANNOTATION_BASE}/allowUsers`]) {
+ constructedService.allowUsers = resource.metadata.annotations[`${ANNOTATION_BASE}/allowUsers`].split(",");
+ }
+ if (resource.metadata.annotations[`${ANNOTATION_BASE}/allowGroups`]) {
+ constructedService.allowGroups = resource.metadata.annotations[`${ANNOTATION_BASE}/allowGroups`].split(",");
+ }
+
+ Object.keys(resource.metadata.annotations).forEach((annotation) => {
+ if (annotation.startsWith(ANNOTATION_WIDGET_BASE)) {
+ shvl.set(
+ constructedService,
+ annotation.replace(`${ANNOTATION_BASE}/`, ""),
+ resource.metadata.annotations[annotation],
+ );
+ }
+ });
+
+ try {
+ constructedService = JSON.parse(substituteEnvironmentVars(JSON.stringify(constructedService)));
+ } catch (e) {
+ logger.error("Error attempting k8s environment variable substitution.");
+ logger.debug(e);
+ }
+
+ return constructedService;
+}
diff --git a/src/utils/kubernetes/traefik-list.js b/src/utils/kubernetes/traefik-list.js
new file mode 100644
index 00000000..f6e07241
--- /dev/null
+++ b/src/utils/kubernetes/traefik-list.js
@@ -0,0 +1,68 @@
+import { CustomObjectsApi } from "@kubernetes/client-node";
+
+import { ANNOTATION_BASE, checkCRD, getKubeConfig, getKubernetes } from "utils/config/kubernetes";
+import createLogger from "utils/logger";
+
+const logger = createLogger("traefik-list");
+const kc = getKubeConfig();
+
+export default async function listTraefikIngress() {
+ const { traefik } = getKubernetes();
+ const traefikList = [];
+
+ if (traefik) {
+ const crd = kc.makeApiClient(CustomObjectsApi);
+ const traefikContainoExists = await checkCRD("ingressroutes.traefik.containo.us", kc, logger);
+ const traefikExists = await checkCRD("ingressroutes.traefik.io", kc, logger);
+
+ const traefikIngressListContaino = await crd
+ .listClusterCustomObject({
+ group: "traefik.containo.us",
+ version: "v1alpha1",
+ plural: "ingressroutes",
+ })
+ .catch(async (error) => {
+ if (traefikContainoExists) {
+ logger.error(
+ "Error getting traefik ingresses from traefik.containo.us: %d %s %s",
+ error.statusCode,
+ error.body,
+ error.response,
+ );
+ logger.debug(error);
+ }
+
+ return [];
+ });
+
+ const traefikIngressListIo = await crd
+ .listClusterCustomObject({
+ group: "traefik.io",
+ version: "v1alpha1",
+ plural: "ingressroutes",
+ })
+ .catch(async (error) => {
+ if (traefikExists) {
+ logger.error(
+ "Error getting traefik ingresses from traefik.io: %d %s %s",
+ error.statusCode,
+ error.body,
+ error.response,
+ );
+ logger.debug(error);
+ }
+
+ return [];
+ });
+
+ const traefikIngressList = [...(traefikIngressListContaino?.items ?? []), ...(traefikIngressListIo?.items ?? [])];
+
+ if (traefikIngressList.length > 0) {
+ const traefikServices = traefikIngressList.filter(
+ (ingress) => ingress.metadata.annotations && ingress.metadata.annotations[`${ANNOTATION_BASE}/href`],
+ );
+ traefikList.push(...traefikServices);
+ }
+ }
+ return traefikList;
+}
diff --git a/src/utils/kubernetes/kubernetes-utils.js b/src/utils/kubernetes/utils.js
similarity index 100%
rename from src/utils/kubernetes/kubernetes-utils.js
rename to src/utils/kubernetes/utils.js
diff --git a/src/utils/proxy/api-helpers.js b/src/utils/proxy/api-helpers.js
index a02ea623..6ef4d9d0 100644
--- a/src/utils/proxy/api-helpers.js
+++ b/src/utils/proxy/api-helpers.js
@@ -2,10 +2,14 @@ export function formatApiCall(url, args) {
const find = /\{.*?\}/g;
const replace = (match) => {
const key = match.replace(/\{|\}/g, "");
- return args[key] || "";
+ let value = args[key];
+ if (key === "url") {
+ value = value.replace(/\/+$/, ""); // remove trailing slashes
+ }
+ return value || "";
};
- return url.replace(/\/+$/, "").replace(find, replace).replace(find, replace);
+ return url.replace(find, replace).replace(find, replace);
}
export function getURLSearchParams(widget, endpoint) {
diff --git a/src/utils/proxy/cached-fetch.js b/src/utils/proxy/cached-fetch.js
deleted file mode 100644
index ae3c4610..00000000
--- a/src/utils/proxy/cached-fetch.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import cache from "memory-cache";
-
-const defaultDuration = 5;
-
-export default async function cachedFetch(url, duration, ua) {
- const cached = cache.get(url);
-
- // eslint-disable-next-line no-param-reassign
- duration = duration || defaultDuration;
-
- if (cached) {
- return cached;
- }
-
- // wrapping text in JSON.parse to handle utf-8 issues
- const options = {};
- if (ua) {
- options.headers = {
- "User-Agent": ua,
- };
- }
- const data = await fetch(url, options).then((res) => res.json());
- cache.put(url, data, duration * 1000 * 60);
- return data;
-}
diff --git a/src/utils/proxy/cookie-jar.js b/src/utils/proxy/cookie-jar.js
index 6519231c..baea21d5 100644
--- a/src/utils/proxy/cookie-jar.js
+++ b/src/utils/proxy/cookie-jar.js
@@ -8,7 +8,7 @@ export function setCookieHeader(url, params) {
const existingCookie = cookieJar.getCookieStringSync(url.toString());
if (existingCookie) {
params.headers = params.headers ?? {};
- params.headers.Cookie = existingCookie;
+ params.headers[params.cookieHeader ?? "Cookie"] = existingCookie;
}
}
diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js
index 01fb313b..017d44c9 100644
--- a/src/utils/proxy/handlers/credentialed.js
+++ b/src/utils/proxy/handlers/credentialed.js
@@ -1,9 +1,9 @@
-import getServiceWidget from "utils/config/service-helpers";
-import { formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers";
-import validateWidgetData from "utils/proxy/validate-widget-data";
-import { httpProxy } from "utils/proxy/http";
-import createLogger from "utils/logger";
import { getSettings } from "utils/config/config";
+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("credentialedProxyHandler");
@@ -41,6 +41,8 @@ export default async function credentialedProxyHandler(req, res, map) {
"cloudflared",
"ghostfolio",
"headscale",
+ "hoarder",
+ "karakeep",
"linkwarden",
"mealie",
"netalertx",
@@ -48,6 +50,7 @@ export default async function credentialedProxyHandler(req, res, map) {
"tandoor",
"pterodactyl",
"vikunja",
+ "firefly",
].includes(widget.type)
) {
headers.Authorization = `Bearer ${widget.key}`;
@@ -98,6 +101,11 @@ export default async function credentialedProxyHandler(req, res, map) {
headers.Authorization = widget.password;
} else if (widget.type === "gitlab") {
headers["PRIVATE-TOKEN"] = widget.key;
+ } else if (widget.type === "speedtest") {
+ if (widget.key) {
+ // v1 does not require a key
+ headers.Authorization = `Bearer ${widget.key}`;
+ }
} else {
headers["X-API-Key"] = `${widget.key}`;
}
diff --git a/src/utils/proxy/handlers/generic.js b/src/utils/proxy/handlers/generic.js
index 2e788a98..1914114c 100644
--- a/src/utils/proxy/handlers/generic.js
+++ b/src/utils/proxy/handlers/generic.js
@@ -1,8 +1,8 @@
import getServiceWidget from "utils/config/service-helpers";
-import { formatApiCall, sanitizeErrorURL } from "utils/proxy/api-helpers";
-import validateWidgetData from "utils/proxy/validate-widget-data";
-import { httpProxy } from "utils/proxy/http";
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("genericProxyHandler");
@@ -79,7 +79,7 @@ export default async function genericProxyHandler(req, res, map) {
error: {
message: "HTTP Error",
url: sanitizeErrorURL(url),
- resultData: Buffer.isBuffer(resultData) ? Buffer.from(resultData).toString() : resultData,
+ data: Buffer.isBuffer(resultData) ? Buffer.from(resultData).toString() : resultData,
},
});
}
diff --git a/src/utils/proxy/handlers/jsonrpc.js b/src/utils/proxy/handlers/jsonrpc.js
index f9fb1883..bdb10e02 100644
--- a/src/utils/proxy/handlers/jsonrpc.js
+++ b/src/utils/proxy/handlers/jsonrpc.js
@@ -1,9 +1,9 @@
import { JSONRPCClient, JSONRPCErrorException } from "json-rpc-2.0";
-import { formatApiCall } from "utils/proxy/api-helpers";
-import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const logger = createLogger("jsonrpcProxyHandler");
diff --git a/src/utils/proxy/handlers/synology.js b/src/utils/proxy/handlers/synology.js
index 030e53ba..6fe98dce 100644
--- a/src/utils/proxy/handlers/synology.js
+++ b/src/utils/proxy/handlers/synology.js
@@ -1,9 +1,9 @@
import cache from "memory-cache";
import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
import { asJson, formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
-import createLogger from "utils/logger";
import widgets from "widgets/widgets";
const INFO_ENDPOINT = "{url}/webapi/query.cgi?api=SYNO.API.Info&version=1&method=query";
diff --git a/src/utils/proxy/http.js b/src/utils/proxy/http.js
index 875bfb4c..1a22a7f7 100644
--- a/src/utils/proxy/http.js
+++ b/src/utils/proxy/http.js
@@ -3,9 +3,10 @@
import { createUnzip, constants as zlibConstants } from "node:zlib";
import { http, https } from "follow-redirects";
+import cache from "memory-cache";
-import { addCookieToJar, setCookieHeader } from "./cookie-jar";
import { sanitizeErrorURL } from "./api-helpers";
+import { addCookieToJar, setCookieHeader } from "./cookie-jar";
import createLogger from "utils/logger";
@@ -81,23 +82,46 @@ export function httpRequest(url, params) {
return handleRequest(http, url, params);
}
+export async function cachedRequest(url, duration = 5, ua = "homepage") {
+ const cached = cache.get(url);
+
+ if (cached) {
+ return cached;
+ }
+
+ const options = {
+ headers: {
+ "User-Agent": ua,
+ Accept: "application/json",
+ },
+ };
+ let [, , data] = await httpProxy(url, options);
+ if (Buffer.isBuffer(data)) {
+ try {
+ data = JSON.parse(Buffer.from(data).toString());
+ } catch (e) {
+ logger.debug("Error parsing cachedRequest data for %s: %s %s", url, Buffer.from(data).toString(), e);
+ data = Buffer.from(data).toString();
+ }
+ }
+ cache.put(url, data, duration * 1000 * 60);
+ return data;
+}
+
export async function httpProxy(url, params = {}) {
const constructedUrl = new URL(url);
+ const disableIpv6 = process.env.HOMEPAGE_PROXY_DISABLE_IPV6 === "true";
+ const agentOptions = disableIpv6 ? { family: 4, autoSelectFamily: false } : {};
let request = null;
if (constructedUrl.protocol === "https:") {
request = httpsRequest(constructedUrl, {
- agent: new https.Agent({
- rejectUnauthorized: false,
- autoSelectFamily: true,
- }),
+ agent: new https.Agent({ ...agentOptions, rejectUnauthorized: false }),
...params,
});
} else {
request = httpRequest(constructedUrl, {
- agent: new http.Agent({
- autoSelectFamily: true,
- }),
+ agent: new http.Agent(agentOptions),
...params,
});
}
diff --git a/src/utils/proxy/validate-widget-data.js b/src/utils/proxy/validate-widget-data.js
index 8f865fe2..de2a3c4e 100644
--- a/src/utils/proxy/validate-widget-data.js
+++ b/src/utils/proxy/validate-widget-data.js
@@ -1,6 +1,6 @@
/* eslint-disable no-console */
-import widgets from "widgets/widgets";
import createLogger from "utils/logger";
+import widgets from "widgets/widgets";
const logger = createLogger("validateWidgetData");
diff --git a/src/widgets/adguard/component.jsx b/src/widgets/adguard/component.jsx
index b2025c4e..e5a7670a 100644
--- a/src/widgets/adguard/component.jsx
+++ b/src/widgets/adguard/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/apcups/component.jsx b/src/widgets/apcups/component.jsx
new file mode 100644
index 00000000..85e621db
--- /dev/null
+++ b/src/widgets/apcups/component.jsx
@@ -0,0 +1,33 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
+export default function Component({ service }) {
+ const { widget } = service;
+ const { data, error } = useWidgetAPI(widget);
+
+ if (error) {
+ return
;
+ }
+
+ if (!data) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/apcups/proxy.js b/src/widgets/apcups/proxy.js
new file mode 100644
index 00000000..bf22730e
--- /dev/null
+++ b/src/widgets/apcups/proxy.js
@@ -0,0 +1,112 @@
+import { Buffer } from "node:buffer";
+import net from "node:net";
+
+import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+
+const logger = createLogger("apcupsProxyHandler");
+
+function parseResponse(buffer) {
+ let ptr = 0;
+ const output = [];
+ while (ptr < buffer.length) {
+ const lineLen = buffer.readUInt16BE(ptr);
+ const asciiData = buffer.toString("ascii", ptr + 2, lineLen + ptr + 2);
+ output.push(asciiData);
+ ptr += 2 + lineLen;
+ }
+
+ return output;
+}
+
+function statusAsJSON(statusOutput) {
+ return statusOutput?.reduce((output, line) => {
+ if (!line || line.startsWith("END APC")) return output;
+ const [key, value] = line.trim().split(":");
+ const newOutput = { ...output };
+ newOutput[key.trim()] = value?.trim();
+ return newOutput;
+ }, {});
+}
+
+async function getStatus(host = "127.0.0.1", port = 3551) {
+ return new Promise((resolve, reject) => {
+ const socket = new net.Socket();
+ socket.setTimeout(5000);
+ socket.connect({ host, port });
+
+ const response = [];
+
+ socket.on("connect", () => {
+ const CMD = "status";
+ logger.debug(`Connecting to ${host}:${port}`);
+ const buffer = Buffer.alloc(CMD.length + 2);
+ buffer.writeUInt16BE(CMD.length, 0);
+ buffer.write(CMD, 2);
+ socket.write(buffer);
+ });
+
+ socket.on("data", (data) => {
+ response.push(data);
+
+ if (data.readUInt16BE(data.length - 2) === 0) {
+ try {
+ const buffer = Buffer.concat(response);
+ const output = parseResponse(buffer);
+ resolve(output);
+ } catch (e) {
+ reject(e);
+ }
+ socket.end();
+ }
+ });
+
+ socket.on("error", (err) => {
+ socket.destroy();
+ reject(err);
+ });
+ socket.on("timeout", () => {
+ socket.destroy();
+ reject(new Error("socket timeout"));
+ });
+ socket.on("end", () => {
+ logger.debug("socket end");
+ });
+ socket.on("close", () => {
+ logger.debug("socket closed");
+ });
+ });
+}
+
+export default async function apcupsProxyHandler(req, res) {
+ const { group, service, index } = req.query;
+
+ if (!group || !service) {
+ logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
+ return res.status(400).json({ error: "Invalid proxy service type" });
+ }
+
+ const widget = await getServiceWidget(group, service, index);
+ if (!widget) {
+ logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
+ return res.status(400).json({ error: "Invalid proxy service type" });
+ }
+
+ const url = new URL(widget.url);
+ const data = {};
+
+ try {
+ const statusData = await getStatus(url.hostname, url.port);
+ const jsonData = statusAsJSON(statusData);
+
+ data.status = jsonData.STATUS;
+ data.load = jsonData.LOADPCT;
+ data.bcharge = jsonData.BCHARGE;
+ data.timeleft = jsonData.TIMELEFT;
+ } catch (e) {
+ logger.error(e);
+ return res.status(500).json({ error: e.message });
+ }
+
+ return res.status(200).send(data);
+}
diff --git a/src/widgets/apcups/widget.js b/src/widgets/apcups/widget.js
new file mode 100644
index 00000000..68f6371b
--- /dev/null
+++ b/src/widgets/apcups/widget.js
@@ -0,0 +1,7 @@
+import apcupsProxyHandler from "./proxy";
+
+const widget = {
+ proxyHandler: apcupsProxyHandler,
+};
+
+export default widget;
diff --git a/src/widgets/argocd/component.jsx b/src/widgets/argocd/component.jsx
index d3b51936..f61bed43 100644
--- a/src/widgets/argocd/component.jsx
+++ b/src/widgets/argocd/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/atsumeru/component.jsx b/src/widgets/atsumeru/component.jsx
index 85e78182..01cc8e46 100644
--- a/src/widgets/atsumeru/component.jsx
+++ b/src/widgets/atsumeru/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/audiobookshelf/component.jsx b/src/widgets/audiobookshelf/component.jsx
index 06439e8f..b410e1a7 100755
--- a/src/widgets/audiobookshelf/component.jsx
+++ b/src/widgets/audiobookshelf/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/audiobookshelf/proxy.js b/src/widgets/audiobookshelf/proxy.js
index 1a89736b..afff3ba9 100644
--- a/src/widgets/audiobookshelf/proxy.js
+++ b/src/widgets/audiobookshelf/proxy.js
@@ -1,7 +1,7 @@
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const proxyName = "audiobookshelfProxyHandler";
diff --git a/src/widgets/authentik/component.jsx b/src/widgets/authentik/component.jsx
index 9ca69c30..edf5ece1 100644
--- a/src/widgets/authentik/component.jsx
+++ b/src/widgets/authentik/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/autobrr/component.jsx b/src/widgets/autobrr/component.jsx
index f983789b..5454cd3c 100644
--- a/src/widgets/autobrr/component.jsx
+++ b/src/widgets/autobrr/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/azuredevops/component.jsx b/src/widgets/azuredevops/component.jsx
index fc388595..7a36aab2 100644
--- a/src/widgets/azuredevops/component.jsx
+++ b/src/widgets/azuredevops/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/bazarr/component.jsx b/src/widgets/bazarr/component.jsx
index 082e368a..f79ec206 100644
--- a/src/widgets/bazarr/component.jsx
+++ b/src/widgets/bazarr/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/bazarr/widget.js b/src/widgets/bazarr/widget.js
index 5b89b2b4..c54e38ad 100644
--- a/src/widgets/bazarr/widget.js
+++ b/src/widgets/bazarr/widget.js
@@ -1,5 +1,5 @@
-import genericProxyHandler from "utils/proxy/handlers/generic";
import { asJson } from "utils/proxy/api-helpers";
+import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/{endpoint}/wanted?apikey={key}",
diff --git a/src/widgets/beszel/component.jsx b/src/widgets/beszel/component.jsx
index da1b7a48..e80a9fab 100644
--- a/src/widgets/beszel/component.jsx
+++ b/src/widgets/beszel/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/beszel/proxy.js b/src/widgets/beszel/proxy.js
index 078e22c3..96dfc913 100644
--- a/src/widgets/beszel/proxy.js
+++ b/src/widgets/beszel/proxy.js
@@ -1,10 +1,10 @@
import cache from "memory-cache";
import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
-import createLogger from "utils/logger";
const proxyName = "beszelProxyHandler";
const tokenCacheKey = `${proxyName}__token`;
diff --git a/src/widgets/caddy/component.jsx b/src/widgets/caddy/component.jsx
index 36e5f959..1613760d 100644
--- a/src/widgets/caddy/component.jsx
+++ b/src/widgets/caddy/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/calendar/agenda.jsx b/src/widgets/calendar/agenda.jsx
index 6a3be031..cb00c1d2 100644
--- a/src/widgets/calendar/agenda.jsx
+++ b/src/widgets/calendar/agenda.jsx
@@ -1,5 +1,5 @@
-import { DateTime } from "luxon";
import classNames from "classnames";
+import { DateTime } from "luxon";
import { useTranslation } from "next-i18next";
import Event, { compareDateTimezone } from "./event";
diff --git a/src/widgets/calendar/component.jsx b/src/widgets/calendar/component.jsx
index ff93c41b..0647d4ad 100644
--- a/src/widgets/calendar/component.jsx
+++ b/src/widgets/calendar/component.jsx
@@ -1,14 +1,13 @@
-import { useEffect, useMemo, useState, useContext } from "react";
-import dynamic from "next/dynamic";
+import Container from "components/services/widget/container";
import { DateTime } from "luxon";
import { useTranslation } from "next-i18next";
-
-import Monthly from "./monthly";
-import Agenda from "./agenda";
-
-import Container from "components/services/widget/container";
+import dynamic from "next/dynamic";
+import { useContext, useEffect, useMemo, useState } from "react";
import { SettingsContext } from "utils/contexts/settings";
+import Agenda from "./agenda";
+import Monthly from "./monthly";
+
const colorVariants = {
// https://tailwindcss.com/docs/content-configuration#dynamic-class-names
amber: "bg-amber-500",
diff --git a/src/widgets/calendar/event.jsx b/src/widgets/calendar/event.jsx
index 91b40e82..6ea2e1ae 100644
--- a/src/widgets/calendar/event.jsx
+++ b/src/widgets/calendar/event.jsx
@@ -1,7 +1,7 @@
-import { useState } from "react";
-import { useTranslation } from "next-i18next";
-import { DateTime } from "luxon";
import classNames from "classnames";
+import { DateTime } from "luxon";
+import { useTranslation } from "next-i18next";
+import { useState } from "react";
import { IoMdCheckmarkCircleOutline } from "react-icons/io";
export default function Event({ event, colorVariants, showDate = false, showTime = false, showDateColumn = true }) {
@@ -26,7 +26,7 @@ export default function Event({ event, colorVariants, showDate = false, showTime
)}
-
+
{hover && event.additional ? event.additional : event.title}
@@ -39,4 +39,5 @@ export default function Event({ event, colorVariants, showDate = false, showTime
);
}
-export const compareDateTimezone = (date, event) => date.startOf("day").ts === event.date.startOf("day").ts;
+export const compareDateTimezone = (date, event) =>
+ date.startOf("day").toISODate() === event.date.startOf("day").toISODate();
diff --git a/src/widgets/calendar/integrations/ical.jsx b/src/widgets/calendar/integrations/ical.jsx
index 059adfa2..46217977 100644
--- a/src/widgets/calendar/integrations/ical.jsx
+++ b/src/widgets/calendar/integrations/ical.jsx
@@ -1,11 +1,11 @@
-import { DateTime } from "luxon";
import { parseString } from "cal-parser";
-import { useEffect } from "react";
+import { DateTime } from "luxon";
import { useTranslation } from "next-i18next";
+import { useEffect } from "react";
import { RRule } from "rrule";
-import useWidgetAPI from "../../../utils/proxy/use-widget-api";
import Error from "../../../components/services/widget/error";
+import useWidgetAPI from "../../../utils/proxy/use-widget-api";
// https://gist.github.com/jlevy/c246006675becc446360a798e2b2d781
function simpleHash(str) {
@@ -51,9 +51,10 @@ export default function Integration({ config, params, setEvents, hideErrors, tim
title = `${config.name}: ${title}`;
}
+ // 'dtend' is null for all-day events
+ const { dtstart, dtend = { value: 0 } } = event;
+
const eventToAdd = (date, i, type) => {
- // 'dtend' is null for all-day events
- const { dtstart, dtend = { value: 0 } } = event;
const days = dtend.value === 0 ? 1 : (dtend.value - dtstart.value) / (1000 * 60 * 60 * 24);
const eventDate = timezone ? DateTime.fromJSDate(date, { zone: timezone }) : DateTime.fromJSDate(date);
@@ -72,13 +73,28 @@ export default function Integration({ config, params, setEvents, hideErrors, tim
}
};
- const recurrenceOptions = event?.recurrenceRule?.origOptions;
+ let recurrenceOptions = event?.recurrenceRule?.origOptions;
+ // RRuleSet does not have dtstart, add it manually
+ if (event?.recurrenceRule && event.recurrenceRule.rrules && event.recurrenceRule.rrules()?.[0]?.origOptions) {
+ recurrenceOptions = event.recurrenceRule.rrules()[0].origOptions;
+ recurrenceOptions.dtstart = dtstart.value;
+ }
+
if (recurrenceOptions && Object.keys(recurrenceOptions).length !== 0) {
try {
const rule = new RRule(recurrenceOptions);
const recurringEvents = rule.between(startDate.toJSDate(), endDate.toJSDate());
- recurringEvents.forEach((date, i) => eventToAdd(date, i, "recurring"));
+ recurringEvents.forEach((date, i) => {
+ let eventDate = date;
+ if (event.dtstart?.params?.tzid) {
+ // date is in UTC but parsed as if it is in current timezone, so we need to adjust it
+ const dateInUTC = DateTime.fromJSDate(date).setZone("UTC");
+ const offset = dateInUTC.offset - DateTime.fromJSDate(date, { zone: event.dtstart.params.tzid }).offset;
+ eventDate = dateInUTC.plus({ minutes: offset }).toJSDate();
+ }
+ eventToAdd(eventDate, i, "recurring");
+ });
return;
} catch (e) {
// eslint-disable-next-line no-console
diff --git a/src/widgets/calendar/integrations/lidarr.jsx b/src/widgets/calendar/integrations/lidarr.jsx
index d4a6edbe..65ad1da2 100644
--- a/src/widgets/calendar/integrations/lidarr.jsx
+++ b/src/widgets/calendar/integrations/lidarr.jsx
@@ -1,8 +1,8 @@
import { DateTime } from "luxon";
import { useEffect } from "react";
-import useWidgetAPI from "../../../utils/proxy/use-widget-api";
import Error from "../../../components/services/widget/error";
+import useWidgetAPI from "../../../utils/proxy/use-widget-api";
export default function Integration({ config, params, setEvents, hideErrors = false }) {
const { data: lidarrData, error: lidarrError } = useWidgetAPI(config, "calendar", {
diff --git a/src/widgets/calendar/integrations/radarr.jsx b/src/widgets/calendar/integrations/radarr.jsx
index 945eadd9..9c8880a9 100644
--- a/src/widgets/calendar/integrations/radarr.jsx
+++ b/src/widgets/calendar/integrations/radarr.jsx
@@ -1,9 +1,9 @@
import { DateTime } from "luxon";
-import { useEffect } from "react";
import { useTranslation } from "next-i18next";
+import { useEffect } from "react";
-import useWidgetAPI from "../../../utils/proxy/use-widget-api";
import Error from "../../../components/services/widget/error";
+import useWidgetAPI from "../../../utils/proxy/use-widget-api";
export default function Integration({ config, params, setEvents, hideErrors = false }) {
const { t } = useTranslation();
diff --git a/src/widgets/calendar/integrations/readarr.jsx b/src/widgets/calendar/integrations/readarr.jsx
index 6ae919ef..4fe3872e 100644
--- a/src/widgets/calendar/integrations/readarr.jsx
+++ b/src/widgets/calendar/integrations/readarr.jsx
@@ -1,8 +1,8 @@
import { DateTime } from "luxon";
import { useEffect } from "react";
-import useWidgetAPI from "../../../utils/proxy/use-widget-api";
import Error from "../../../components/services/widget/error";
+import useWidgetAPI from "../../../utils/proxy/use-widget-api";
export default function Integration({ config, params, setEvents, hideErrors = false }) {
const { data: readarrData, error: readarrError } = useWidgetAPI(config, "calendar", {
diff --git a/src/widgets/calendar/integrations/sonarr.jsx b/src/widgets/calendar/integrations/sonarr.jsx
index 34cc679d..abdec328 100644
--- a/src/widgets/calendar/integrations/sonarr.jsx
+++ b/src/widgets/calendar/integrations/sonarr.jsx
@@ -1,8 +1,8 @@
import { DateTime } from "luxon";
import { useEffect } from "react";
-import useWidgetAPI from "../../../utils/proxy/use-widget-api";
import Error from "../../../components/services/widget/error";
+import useWidgetAPI from "../../../utils/proxy/use-widget-api";
export default function Integration({ config, params, setEvents, hideErrors = false }) {
const { data: sonarrData, error: sonarrError } = useWidgetAPI(config, "calendar", {
diff --git a/src/widgets/calendar/monthly.jsx b/src/widgets/calendar/monthly.jsx
index a102e8da..4e870261 100644
--- a/src/widgets/calendar/monthly.jsx
+++ b/src/widgets/calendar/monthly.jsx
@@ -1,7 +1,7 @@
-import { useMemo } from "react";
-import { DateTime, Info } from "luxon";
import classNames from "classnames";
+import { DateTime, Info } from "luxon";
import { useTranslation } from "next-i18next";
+import { useMemo } from "react";
import Event, { compareDateTimezone } from "./event";
@@ -57,7 +57,7 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS
.map((event) => (
))}
diff --git a/src/widgets/calendar/proxy.js b/src/widgets/calendar/proxy.js
index d36f30c9..c98441ee 100644
--- a/src/widgets/calendar/proxy.js
+++ b/src/widgets/calendar/proxy.js
@@ -1,6 +1,6 @@
import getServiceWidget from "utils/config/service-helpers";
-import { httpProxy } from "utils/proxy/http";
import createLogger from "utils/logger";
+import { httpProxy } from "utils/proxy/http";
const logger = createLogger("calendarProxyHandler");
diff --git a/src/widgets/calibreweb/component.jsx b/src/widgets/calibreweb/component.jsx
index acb9e029..be8424e5 100644
--- a/src/widgets/calibreweb/component.jsx
+++ b/src/widgets/calibreweb/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/changedetectionio/component.jsx b/src/widgets/changedetectionio/component.jsx
index caef8655..d7d7272b 100644
--- a/src/widgets/changedetectionio/component.jsx
+++ b/src/widgets/changedetectionio/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/channelsdvrserver/component.jsx b/src/widgets/channelsdvrserver/component.jsx
index 52e94c29..79ca3f14 100644
--- a/src/widgets/channelsdvrserver/component.jsx
+++ b/src/widgets/channelsdvrserver/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/cloudflared/component.jsx b/src/widgets/cloudflared/component.jsx
index e9200171..790a5f34 100644
--- a/src/widgets/cloudflared/component.jsx
+++ b/src/widgets/cloudflared/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/coinmarketcap/component.jsx b/src/widgets/coinmarketcap/component.jsx
index 7e717bef..fd9c030f 100644
--- a/src/widgets/coinmarketcap/component.jsx
+++ b/src/widgets/coinmarketcap/component.jsx
@@ -1,10 +1,10 @@
-import { useState } from "react";
-import { useTranslation } from "next-i18next";
import classNames from "classnames";
-
-import Container from "components/services/widget/container";
-import Block from "components/services/widget/block";
import Dropdown from "components/services/dropdown";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+import { useTranslation } from "next-i18next";
+import { useState } from "react";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
@@ -73,7 +73,7 @@ export default function Component({ service }) {
{validCryptos.map((crypto) => (
{crypto.name}
diff --git a/src/widgets/components.js b/src/widgets/components.js
index 19f41d4a..880c8222 100644
--- a/src/widgets/components.js
+++ b/src/widgets/components.js
@@ -2,6 +2,7 @@ import dynamic from "next/dynamic";
const components = {
adguard: dynamic(() => import("./adguard/component")),
+ apcups: dynamic(() => import("./apcups/component")),
argocd: dynamic(() => import("./argocd/component")),
atsumeru: dynamic(() => import("./atsumeru/component")),
audiobookshelf: dynamic(() => import("./audiobookshelf/component")),
@@ -30,6 +31,7 @@ const components = {
esphome: dynamic(() => import("./esphome/component")),
evcc: dynamic(() => import("./evcc/component")),
fileflows: dynamic(() => import("./fileflows/component")),
+ firefly: dynamic(() => import("./firefly/component")),
flood: dynamic(() => import("./flood/component")),
freshrss: dynamic(() => import("./freshrss/component")),
frigate: dynamic(() => import("./frigate/component")),
@@ -45,6 +47,8 @@ const components = {
grafana: dynamic(() => import("./grafana/component")),
hdhomerun: dynamic(() => import("./hdhomerun/component")),
headscale: dynamic(() => import("./headscale/component")),
+ hoarder: dynamic(() => import("./karakeep/component")),
+ karakeep: dynamic(() => import("./karakeep/component")),
peanut: dynamic(() => import("./peanut/component")),
homeassistant: dynamic(() => import("./homeassistant/component")),
homebox: dynamic(() => import("./homebox/component")),
@@ -110,6 +114,7 @@ const components = {
rutorrent: dynamic(() => import("./rutorrent/component")),
sabnzbd: dynamic(() => import("./sabnzbd/component")),
scrutiny: dynamic(() => import("./scrutiny/component")),
+ slskd: dynamic(() => import("./slskd/component")),
sonarr: dynamic(() => import("./sonarr/component")),
speedtest: dynamic(() => import("./speedtest/component")),
spoolman: dynamic(() => import("./spoolman/component")),
diff --git a/src/widgets/crowdsec/component.jsx b/src/widgets/crowdsec/component.jsx
index 2e98cee9..f567ad70 100644
--- a/src/widgets/crowdsec/component.jsx
+++ b/src/widgets/crowdsec/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/crowdsec/proxy.js b/src/widgets/crowdsec/proxy.js
index 85803845..d3257fa6 100644
--- a/src/widgets/crowdsec/proxy.js
+++ b/src/widgets/crowdsec/proxy.js
@@ -1,9 +1,9 @@
import cache from "memory-cache";
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const proxyName = "crowdsecProxyHandler";
diff --git a/src/widgets/customapi/component.jsx b/src/widgets/customapi/component.jsx
index 3f1825b2..f537e8da 100644
--- a/src/widgets/customapi/component.jsx
+++ b/src/widgets/customapi/component.jsx
@@ -1,7 +1,9 @@
+import classNames from "classnames";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
-import Container from "components/services/widget/container";
-import Block from "components/services/widget/block";
+import * as shvl from "utils/config/shvl";
import useWidgetAPI from "utils/proxy/use-widget-api";
function getValue(field, data) {
@@ -14,6 +16,11 @@ function getValue(field, data) {
return value;
}
+ // shvl is easier, everything else is kept for backwards compatibility.
+ if (typeof field === "string") {
+ return shvl.get(data, field, null) ?? data[field] ?? null;
+ }
+
while (typeof lastField === "object") {
key = Object.keys(lastField)[0] ?? null;
@@ -78,6 +85,9 @@ function formatValue(t, mapping, rawValue) {
case "percent":
value = t("common.percent", { value });
break;
+ case "duration":
+ value = t("common.duration", { value });
+ break;
case "bytes":
value = t("common.bytes", { value });
break;
@@ -162,6 +172,16 @@ export default function Component({ service }) {
if (!customData) {
switch (display) {
+ case "dynamic-list":
+ return (
+
+
+
+ );
case "list":
return (
@@ -169,7 +189,7 @@ export default function Component({ service }) {
{mappings.map((mapping) => (
{mapping.label}
@@ -193,6 +213,76 @@ export default function Component({ service }) {
}
switch (display) {
+ case "dynamic-list":
+ let listItems = customData;
+ if (mappings.items) listItems = shvl.get(customData, mappings.items, null);
+ let error;
+ if (!listItems || !Array.isArray(listItems)) {
+ error = { message: "Unable to find items" };
+ }
+ const name = mappings.name;
+ const label = mappings.label;
+ if (!name || !label) {
+ error = { message: "Name and label properties are required" };
+ }
+ if (error) {
+ return
;
+ }
+
+ const target = mappings.target;
+ if (mappings.limit && parseInt(mappings.limit, 10) > 0) {
+ listItems.splice(mappings.limit);
+ }
+
+ return (
+
+
+ {listItems.length === 0 ? (
+
+ ) : (
+ listItems.map((item, index) => {
+ const itemName = shvl.get(item, name, item[name]) ?? "";
+ const itemLabel = shvl.get(item, label, item[label]) ?? "";
+
+ const itemUrl = target
+ ? [...target.matchAll(/\{(.*?)\}/g)]
+ .map((match) => match[1])
+ .reduce((url, targetTemplate) => {
+ const value = shvl.get(item, targetTemplate, item[targetTemplate]) ?? "";
+ return url.replaceAll(`{${targetTemplate}}`, value);
+ }, target)
+ : null;
+ const className =
+ "bg-theme-200/50 dark:bg-theme-900/20 rounded-sm m-1 flex-1 flex flex-row items-center justify-between p-1 text-xs";
+
+ return itemUrl ? (
+
+ {itemName}
+
+
{formatValue(t, mappings, itemLabel)}
+
+
+ ) : (
+
+
{itemName}
+
+
{formatValue(t, mappings, itemLabel)}
+
+
+ );
+ })
+ )}
+
+
+ );
case "list":
return (
@@ -200,7 +290,7 @@ export default function Component({ service }) {
{mappings.map((mapping) => (
{mapping.label}
diff --git a/src/widgets/deluge/component.jsx b/src/widgets/deluge/component.jsx
index 231f69ab..eb6ddfaa 100644
--- a/src/widgets/deluge/component.jsx
+++ b/src/widgets/deluge/component.jsx
@@ -1,9 +1,9 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
import QueueEntry from "../../components/widgets/queue/queueEntry";
-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 }) {
diff --git a/src/widgets/deluge/proxy.js b/src/widgets/deluge/proxy.js
index 0430a6ac..ef255160 100644
--- a/src/widgets/deluge/proxy.js
+++ b/src/widgets/deluge/proxy.js
@@ -1,7 +1,7 @@
-import { formatApiCall } from "utils/proxy/api-helpers";
-import { sendJsonRpcRequest } from "utils/proxy/handlers/jsonrpc";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { sendJsonRpcRequest } from "utils/proxy/handlers/jsonrpc";
import widgets from "widgets/widgets";
const logger = createLogger("delugeProxyHandler");
diff --git a/src/widgets/develancacheui/component.jsx b/src/widgets/develancacheui/component.jsx
index b13852c6..61f608e1 100644
--- a/src/widgets/develancacheui/component.jsx
+++ b/src/widgets/develancacheui/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/diskstation/component.jsx b/src/widgets/diskstation/component.jsx
index 26550466..0ca0b8ae 100644
--- a/src/widgets/diskstation/component.jsx
+++ b/src/widgets/diskstation/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
@@ -43,8 +43,9 @@ export default function Component({ service }) {
// utilization info
const { cpu, memory } = utilizationData.data;
const cpuLoad = parseFloat(cpu.user_load) + parseFloat(cpu.system_load);
- const memoryUsage =
- 100 - (100 * (parseFloat(memory.avail_real) + parseFloat(memory.cached))) / parseFloat(memory.total_real);
+ const memoryUsage = memory.real_usage
+ ? parseFloat(memory.real_usage)
+ : 100 - (100 * (parseFloat(memory.avail_real) + parseFloat(memory.cached))) / parseFloat(memory.total_real);
return (
diff --git a/src/widgets/docker/component.jsx b/src/widgets/docker/component.jsx
index 9535d3bd..6e05454f 100644
--- a/src/widgets/docker/component.jsx
+++ b/src/widgets/docker/component.jsx
@@ -1,10 +1,9 @@
-import useSWR from "swr";
-import { useTranslation } from "next-i18next";
-
-import { calculateCPUPercent, calculateUsedMemory, calculateThroughput } from "./stats-helpers";
-
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+import { useTranslation } from "next-i18next";
+import useSWR from "swr";
+
+import { calculateCPUPercent, calculateThroughput, calculateUsedMemory } from "./stats-helpers";
export default function Component({ service }) {
const { t } = useTranslation();
diff --git a/src/widgets/downloadstation/component.jsx b/src/widgets/downloadstation/component.jsx
index aae4713d..016f4953 100644
--- a/src/widgets/downloadstation/component.jsx
+++ b/src/widgets/downloadstation/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/emby/component.jsx b/src/widgets/emby/component.jsx
index 6f66d1dc..88858da2 100644
--- a/src/widgets/emby/component.jsx
+++ b/src/widgets/emby/component.jsx
@@ -1,9 +1,9 @@
-import { useTranslation } from "next-i18next";
-import { BsVolumeMuteFill, BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
-import { MdOutlineSmartDisplay } from "react-icons/md";
-
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
+import { useTranslation } from "next-i18next";
+import { BsCpu, BsFillCpuFill, BsFillPlayFill, BsPauseFill, BsVolumeMuteFill } from "react-icons/bs";
+import { MdOutlineSmartDisplay } from "react-icons/md";
+
import { getURLSearchParams } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -35,8 +35,8 @@ function generateStreamTitle(session, enableUser, showEpisodeNumber) {
let streamTitle = "";
if (Type === "Episode" && showEpisodeNumber) {
- const seasonStr = `S${ParentIndexNumber.toString().padStart(2, "0")}`;
- const episodeStr = `E${IndexNumber.toString().padStart(2, "0")}`;
+ const seasonStr = ParentIndexNumber ? `S${ParentIndexNumber.toString().padStart(2, "0")}` : "";
+ const episodeStr = IndexNumber ? `E${IndexNumber.toString().padStart(2, "0")}` : "";
streamTitle = `${SeriesName}: ${seasonStr} · ${episodeStr} - ${Name}`;
} else {
streamTitle = `${Name}${SeriesName ? ` - ${SeriesName}` : ""}`;
diff --git a/src/widgets/esphome/component.jsx b/src/widgets/esphome/component.jsx
index ea2e5db3..e0f02089 100644
--- a/src/widgets/esphome/component.jsx
+++ b/src/widgets/esphome/component.jsx
@@ -1,7 +1,7 @@
-import { useTranslation } from "next-i18next";
-
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";
export default function Component({ service }) {
diff --git a/src/widgets/evcc/component.jsx b/src/widgets/evcc/component.jsx
index 09a8ffe4..d0debdc3 100644
--- a/src/widgets/evcc/component.jsx
+++ b/src/widgets/evcc/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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";
function toKilowatts(t, value) {
@@ -29,10 +29,13 @@ export default function Component({ service }) {
);
}
+ // broken by evcc v0.133.0 https://github.com/evcc-io/evcc/commit/9dcb1fa0a7c08dd926b79309aa1f676a5fc6c8aa
+ const gridPower = stateData.result.gridPower ?? stateData.result.grid?.power ?? 0;
+
return (
-
+
;
+ }
+
+ if (!summaryData || !budgetData) {
+ return (
+
+
+
+
+ );
+ }
+
+ const netWorth = Object.keys(summaryData)
+ .filter((key) => key.includes("net-worth-in"))
+ .map((key) => summaryData[key]);
+
+ let budgetValue = null;
+
+ if (budgetData.data?.length && budgetData.data[0].type === "available_budgets") {
+ const budgetAmount = parseFloat(budgetData.data[0].attributes.amount);
+ const budgetSpent = -parseFloat(budgetData.data[0].attributes.spent_in_budgets[0]?.sum ?? "0");
+ const budgetCurrency = budgetData.data[0].attributes.currency_symbol;
+
+ budgetValue = `${budgetCurrency} ${t("common.number", {
+ value: budgetSpent,
+ minimumFractionDigits: 2,
+ })} / ${budgetCurrency} ${t("common.number", {
+ value: budgetAmount,
+ minimumFractionDigits: 2,
+ })}`;
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/widgets/firefly/widget.js b/src/widgets/firefly/widget.js
new file mode 100644
index 00000000..cd23504d
--- /dev/null
+++ b/src/widgets/firefly/widget.js
@@ -0,0 +1,19 @@
+import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
+
+const widget = {
+ api: "{url}/api/{endpoint}",
+ proxyHandler: credentialedProxyHandler,
+
+ mappings: {
+ summary: {
+ endpoint: "v1/summary/basic",
+ params: ["start", "end"],
+ },
+ budgets: {
+ endpoint: "v1/available-budgets",
+ params: ["start", "end"],
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/flood/component.jsx b/src/widgets/flood/component.jsx
index 4ecdcbcd..92a2b61a 100644
--- a/src/widgets/flood/component.jsx
+++ b/src/widgets/flood/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/flood/proxy.js b/src/widgets/flood/proxy.js
index e0c10173..5e5335ae 100644
--- a/src/widgets/flood/proxy.js
+++ b/src/widgets/flood/proxy.js
@@ -1,7 +1,7 @@
-import { formatApiCall } from "utils/proxy/api-helpers";
-import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
const logger = createLogger("floodProxyHandler");
diff --git a/src/widgets/freshrss/component.jsx b/src/widgets/freshrss/component.jsx
index 788f1f6f..70833e1d 100644
--- a/src/widgets/freshrss/component.jsx
+++ b/src/widgets/freshrss/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/freshrss/proxy.js b/src/widgets/freshrss/proxy.js
index 881094bd..6168db86 100644
--- a/src/widgets/freshrss/proxy.js
+++ b/src/widgets/freshrss/proxy.js
@@ -1,9 +1,9 @@
import cache from "memory-cache";
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const proxyName = "freshrssProxyHandler";
diff --git a/src/widgets/frigate/component.jsx b/src/widgets/frigate/component.jsx
index ac77be51..ab67c461 100644
--- a/src/widgets/frigate/component.jsx
+++ b/src/widgets/frigate/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/fritzbox/component.jsx b/src/widgets/fritzbox/component.jsx
index c3ea1bf6..d7928c20 100644
--- a/src/widgets/fritzbox/component.jsx
+++ b/src/widgets/fritzbox/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 const fritzboxDefaultFields = ["connectionStatus", "uptime", "maxDown", "maxUp"];
@@ -37,6 +37,8 @@ export default function Component({ service }) {
+
+
);
}
@@ -52,6 +54,8 @@ export default function Component({ service }) {
+
+
);
}
diff --git a/src/widgets/fritzbox/proxy.js b/src/widgets/fritzbox/proxy.js
index d1a66d97..c8c57fbc 100644
--- a/src/widgets/fritzbox/proxy.js
+++ b/src/widgets/fritzbox/proxy.js
@@ -2,9 +2,9 @@ import { xml2json } from "xml-js";
import { fritzboxDefaultFields } from "./component";
-import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { httpProxy } from "utils/proxy/http";
const logger = createLogger("fritzboxProxyHandler");
@@ -70,14 +70,21 @@ export default async function fritzboxProxyHandler(req, res) {
const requestLinkProperties = ["maxDown", "maxUp"].some((field) => serviceWidget.fields.includes(field));
const requestAddonInfos = ["down", "up", "received", "sent"].some((field) => serviceWidget.fields.includes(field));
const requestExternalIPAddress = ["externalIPAddress"].some((field) => serviceWidget.fields.includes(field));
+ const requestExternalIPv6Address = ["externalIPv6Address"].some((field) => serviceWidget.fields.includes(field));
+ const requestExternalIPv6Prefix = ["externalIPv6Prefix"].some((field) => serviceWidget.fields.includes(field));
await Promise.all([
+ // as per http://fritz.box:49000/igddesc.xml specifications (fritz.box is a hostname of your router)
requestStatusInfo ? requestEndpoint(apiBaseUrl, "WANIPConnection", "GetStatusInfo") : null,
requestLinkProperties ? requestEndpoint(apiBaseUrl, "WANCommonInterfaceConfig", "GetCommonLinkProperties") : null,
requestAddonInfos ? requestEndpoint(apiBaseUrl, "WANCommonInterfaceConfig", "GetAddonInfos") : null,
requestExternalIPAddress ? requestEndpoint(apiBaseUrl, "WANIPConnection", "GetExternalIPAddress") : null,
+ requestExternalIPv6Address
+ ? requestEndpoint(apiBaseUrl, "WANIPConnection", "X_AVM_DE_GetExternalIPv6Address")
+ : null,
+ requestExternalIPv6Prefix ? requestEndpoint(apiBaseUrl, "WANIPConnection", "X_AVM_DE_GetIPv6Prefix") : null,
])
- .then(([statusInfo, linkProperties, addonInfos, externalIPAddress]) => {
+ .then(([statusInfo, linkProperties, addonInfos, externalIPAddress, externalIPv6Address, externalIPv6Prefix]) => {
res.status(200).json({
connectionStatus: statusInfo?.NewConnectionStatus || "Unconfigured",
uptime: statusInfo?.NewUptime || 0,
@@ -88,6 +95,8 @@ export default async function fritzboxProxyHandler(req, res) {
received: addonInfos?.NewX_AVM_DE_TotalBytesReceived64 || 0,
sent: addonInfos?.NewX_AVM_DE_TotalBytesSent64 || 0,
externalIPAddress: externalIPAddress?.NewExternalIPAddress || null,
+ externalIPv6Address: externalIPv6Address?.NewExternalIPv6Address || null,
+ externalIPv6Prefix: externalIPv6Prefix?.NewIPv6Prefix || null,
});
})
.catch((error) => {
diff --git a/src/widgets/gamedig/component.jsx b/src/widgets/gamedig/component.jsx
index 69b78d9a..5acd1b9c 100644
--- a/src/widgets/gamedig/component.jsx
+++ b/src/widgets/gamedig/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/gamedig/proxy.js b/src/widgets/gamedig/proxy.js
index ecf6e4c6..79d7fa02 100644
--- a/src/widgets/gamedig/proxy.js
+++ b/src/widgets/gamedig/proxy.js
@@ -1,7 +1,7 @@
import { GameDig } from "gamedig";
-import createLogger from "utils/logger";
import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
const proxyName = "gamedigProxyHandler";
const logger = createLogger(proxyName);
@@ -24,7 +24,7 @@ export default async function gamedigProxyHandler(req, res) {
online: true,
name: serverData.name,
map: serverData.map,
- players: serverData.players.length,
+ players: serverData.numplayers ?? serverData.players?.length,
maxplayers: serverData.maxplayers,
bots: serverData.bots.length,
ping: serverData.ping,
diff --git a/src/widgets/gatus/component.jsx b/src/widgets/gatus/component.jsx
index 86b85ff3..25aae239 100644
--- a/src/widgets/gatus/component.jsx
+++ b/src/widgets/gatus/component.jsx
@@ -1,8 +1,8 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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();
diff --git a/src/widgets/ghostfolio/component.jsx b/src/widgets/ghostfolio/component.jsx
index 183b05ca..f2587586 100644
--- a/src/widgets/ghostfolio/component.jsx
+++ b/src/widgets/ghostfolio/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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";
function getPerformancePercent(t, performanceRange) {
diff --git a/src/widgets/gitea/component.jsx b/src/widgets/gitea/component.jsx
index b193efd2..81b69d67 100644
--- a/src/widgets/gitea/component.jsx
+++ b/src/widgets/gitea/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
@@ -7,17 +8,21 @@ export default function Component({ service }) {
const { data: giteaNotifications, error: giteaNotificationsError } = useWidgetAPI(widget, "notifications");
const { data: giteaIssues, error: giteaIssuesError } = useWidgetAPI(widget, "issues");
+ const { data: giteaRepositories, error: giteaRepositoriesError } = useWidgetAPI(widget, "repositories");
- if (giteaNotificationsError || giteaIssuesError) {
- return
;
+ if (giteaNotificationsError || giteaIssuesError || giteaRepositoriesError) {
+ return (
+
+ );
}
- if (!giteaNotifications || !giteaIssues) {
+ if (!giteaNotifications || !giteaIssues || !giteaRepositories) {
return (
+
);
}
@@ -27,6 +32,7 @@ export default function Component({ service }) {
+
);
}
diff --git a/src/widgets/gitea/widget.js b/src/widgets/gitea/widget.js
index 32871b00..b0420ccc 100644
--- a/src/widgets/gitea/widget.js
+++ b/src/widgets/gitea/widget.js
@@ -16,6 +16,9 @@ const widget = {
issues: asJson(data).filter((issue) => !issue.pull_request),
}),
},
+ repositories: {
+ endpoint: "repos/search",
+ },
},
};
diff --git a/src/widgets/gitlab/component.jsx b/src/widgets/gitlab/component.jsx
index fb6f898f..4d2805ba 100644
--- a/src/widgets/gitlab/component.jsx
+++ b/src/widgets/gitlab/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/glances/component.jsx b/src/widgets/glances/component.jsx
index bff31ac1..4ca0cc72 100644
--- a/src/widgets/glances/component.jsx
+++ b/src/widgets/glances/component.jsx
@@ -1,13 +1,13 @@
-import Memory from "./metrics/memory";
+import Containers from "./metrics/containers";
import Cpu from "./metrics/cpu";
-import Sensor from "./metrics/sensor";
-import Net from "./metrics/net";
-import Process from "./metrics/process";
import Disk from "./metrics/disk";
+import Fs from "./metrics/fs";
import GPU from "./metrics/gpu";
import Info from "./metrics/info";
-import Fs from "./metrics/fs";
-import Containers from "./metrics/containers";
+import Memory from "./metrics/memory";
+import Net from "./metrics/net";
+import Process from "./metrics/process";
+import Sensor from "./metrics/sensor";
export default function Component({ service }) {
const { widget } = service;
diff --git a/src/widgets/glances/components/chart.jsx b/src/widgets/glances/components/chart.jsx
index 132fcc8e..b919a92d 100644
--- a/src/widgets/glances/components/chart.jsx
+++ b/src/widgets/glances/components/chart.jsx
@@ -1,5 +1,5 @@
import { PureComponent } from "react";
-import { AreaChart, Area, ResponsiveContainer, Tooltip } from "recharts";
+import { Area, AreaChart, ResponsiveContainer, Tooltip } from "recharts";
import CustomTooltip from "./custom_tooltip";
diff --git a/src/widgets/glances/components/chart_dual.jsx b/src/widgets/glances/components/chart_dual.jsx
index 5fabe755..d6ec2076 100644
--- a/src/widgets/glances/components/chart_dual.jsx
+++ b/src/widgets/glances/components/chart_dual.jsx
@@ -1,5 +1,5 @@
import { PureComponent } from "react";
-import { AreaChart, Area, ResponsiveContainer, Tooltip } from "recharts";
+import { Area, AreaChart, ResponsiveContainer, Tooltip } from "recharts";
import CustomTooltip from "./custom_tooltip";
diff --git a/src/widgets/glances/components/container.jsx b/src/widgets/glances/components/container.jsx
index 01a7e05d..7bcd5c46 100644
--- a/src/widgets/glances/components/container.jsx
+++ b/src/widgets/glances/components/container.jsx
@@ -1,10 +1,9 @@
-import { useContext } from "react";
import classNames from "classnames";
+import { useContext } from "react";
+import { SettingsContext } from "utils/contexts/settings";
import Error from "./error";
-import { SettingsContext } from "utils/contexts/settings";
-
export default function Container({ children, widget, error = null, chart = true, className = "" }) {
const { settings } = useContext(SettingsContext);
const hideErrors = settings.hideErrors || widget?.hideErrors;
diff --git a/src/widgets/glances/metrics/containers.jsx b/src/widgets/glances/metrics/containers.jsx
index 1f408d3e..93ecbc28 100644
--- a/src/widgets/glances/metrics/containers.jsx
+++ b/src/widgets/glances/metrics/containers.jsx
@@ -1,10 +1,10 @@
+import ResolvedIcon from "components/resolvedicon";
import { useTranslation } from "next-i18next";
-import Container from "../components/container";
import Block from "../components/block";
+import Container from "../components/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
-import ResolvedIcon from "components/resolvedicon";
const statusMap = {
running:
,
@@ -65,7 +65,7 @@ export default function Component({ service }) {
{item.cpu_percent.toFixed(1)}%
{t("common.bytes", {
- value: item.memory.usage,
+ value: item.memory.usage - item.memory.inactive_file,
maximumFractionDigits: 0,
})}
diff --git a/src/widgets/glances/metrics/cpu.jsx b/src/widgets/glances/metrics/cpu.jsx
index e993fca9..3debf11a 100644
--- a/src/widgets/glances/metrics/cpu.jsx
+++ b/src/widgets/glances/metrics/cpu.jsx
@@ -1,9 +1,9 @@
-import dynamic from "next/dynamic";
-import { useState, useEffect } from "react";
import { useTranslation } from "next-i18next";
+import dynamic from "next/dynamic";
+import { useEffect, useState } from "react";
-import Container from "../components/container";
import Block from "../components/block";
+import Container from "../components/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
diff --git a/src/widgets/glances/metrics/disk.jsx b/src/widgets/glances/metrics/disk.jsx
index 0a459e07..69dd2d99 100644
--- a/src/widgets/glances/metrics/disk.jsx
+++ b/src/widgets/glances/metrics/disk.jsx
@@ -1,9 +1,9 @@
-import dynamic from "next/dynamic";
-import { useState, useEffect } from "react";
import { useTranslation } from "next-i18next";
+import dynamic from "next/dynamic";
+import { useEffect, useState } from "react";
-import Container from "../components/container";
import Block from "../components/block";
+import Container from "../components/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
diff --git a/src/widgets/glances/metrics/fs.jsx b/src/widgets/glances/metrics/fs.jsx
index 3285bd0e..317a781f 100644
--- a/src/widgets/glances/metrics/fs.jsx
+++ b/src/widgets/glances/metrics/fs.jsx
@@ -1,7 +1,7 @@
import { useTranslation } from "next-i18next";
-import Container from "../components/container";
import Block from "../components/block";
+import Container from "../components/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -46,9 +46,9 @@ export default function Component({ service }) {
)}
diff --git a/src/widgets/glances/metrics/gpu.jsx b/src/widgets/glances/metrics/gpu.jsx
index 37b06ce3..7eab536c 100644
--- a/src/widgets/glances/metrics/gpu.jsx
+++ b/src/widgets/glances/metrics/gpu.jsx
@@ -1,9 +1,9 @@
-import dynamic from "next/dynamic";
-import { useState, useEffect } from "react";
import { useTranslation } from "next-i18next";
+import dynamic from "next/dynamic";
+import { useEffect, useState } from "react";
-import Container from "../components/container";
import Block from "../components/block";
+import Container from "../components/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
diff --git a/src/widgets/glances/metrics/info.jsx b/src/widgets/glances/metrics/info.jsx
index 3fc7ab23..3c0ef429 100644
--- a/src/widgets/glances/metrics/info.jsx
+++ b/src/widgets/glances/metrics/info.jsx
@@ -1,7 +1,7 @@
import { useTranslation } from "next-i18next";
-import Container from "../components/container";
import Block from "../components/block";
+import Container from "../components/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -109,7 +109,7 @@ export default function Component({ service }) {
return (
{chart && (
-
+
)}
diff --git a/src/widgets/glances/metrics/memory.jsx b/src/widgets/glances/metrics/memory.jsx
index 8cfddb66..e5fbc350 100644
--- a/src/widgets/glances/metrics/memory.jsx
+++ b/src/widgets/glances/metrics/memory.jsx
@@ -1,9 +1,9 @@
-import dynamic from "next/dynamic";
-import { useState, useEffect } from "react";
import { useTranslation } from "next-i18next";
+import dynamic from "next/dynamic";
+import { useEffect, useState } from "react";
-import Container from "../components/container";
import Block from "../components/block";
+import Container from "../components/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
diff --git a/src/widgets/glances/metrics/net.jsx b/src/widgets/glances/metrics/net.jsx
index 372c4ec6..2bdd491c 100644
--- a/src/widgets/glances/metrics/net.jsx
+++ b/src/widgets/glances/metrics/net.jsx
@@ -1,9 +1,9 @@
-import dynamic from "next/dynamic";
-import { useState, useEffect } from "react";
import { useTranslation } from "next-i18next";
+import dynamic from "next/dynamic";
+import { useEffect, useState } from "react";
-import Container from "../components/container";
import Block from "../components/block";
+import Container from "../components/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
diff --git a/src/widgets/glances/metrics/process.jsx b/src/widgets/glances/metrics/process.jsx
index 997948f4..ad3fee54 100644
--- a/src/widgets/glances/metrics/process.jsx
+++ b/src/widgets/glances/metrics/process.jsx
@@ -1,10 +1,10 @@
+import ResolvedIcon from "components/resolvedicon";
import { useTranslation } from "next-i18next";
-import Container from "../components/container";
import Block from "../components/block";
+import Container from "../components/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
-import ResolvedIcon from "components/resolvedicon";
const statusMap = {
R: , // running
diff --git a/src/widgets/glances/metrics/sensor.jsx b/src/widgets/glances/metrics/sensor.jsx
index 3cb38c1c..b5a16d10 100644
--- a/src/widgets/glances/metrics/sensor.jsx
+++ b/src/widgets/glances/metrics/sensor.jsx
@@ -1,9 +1,9 @@
-import dynamic from "next/dynamic";
-import { useState, useEffect } from "react";
import { useTranslation } from "next-i18next";
+import dynamic from "next/dynamic";
+import { useEffect, useState } from "react";
-import Container from "../components/container";
import Block from "../components/block";
+import Container from "../components/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
diff --git a/src/widgets/gluetun/component.jsx b/src/widgets/gluetun/component.jsx
index c4ec14fb..f7128237 100644
--- a/src/widgets/gluetun/component.jsx
+++ b/src/widgets/gluetun/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/gotify/component.jsx b/src/widgets/gotify/component.jsx
index 39664a31..6cf9cea9 100644
--- a/src/widgets/gotify/component.jsx
+++ b/src/widgets/gotify/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/grafana/component.jsx b/src/widgets/grafana/component.jsx
index ecedac8e..82d6e5c9 100755
--- a/src/widgets/grafana/component.jsx
+++ b/src/widgets/grafana/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/hdhomerun/component.jsx b/src/widgets/hdhomerun/component.jsx
index a118eafe..2532f92b 100644
--- a/src/widgets/hdhomerun/component.jsx
+++ b/src/widgets/hdhomerun/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/headscale/component.jsx b/src/widgets/headscale/component.jsx
index 361aa756..acee3a82 100644
--- a/src/widgets/headscale/component.jsx
+++ b/src/widgets/headscale/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/healthchecks/component.jsx b/src/widgets/healthchecks/component.jsx
index 21fb7cb6..b65f91c5 100644
--- a/src/widgets/healthchecks/component.jsx
+++ b/src/widgets/healthchecks/component.jsx
@@ -1,9 +1,9 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
import { i18n } from "../../../next-i18next.config";
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function formatDate(dateString) {
diff --git a/src/widgets/homeassistant/component.jsx b/src/widgets/homeassistant/component.jsx
index af75a664..1df415ae 100644
--- a/src/widgets/homeassistant/component.jsx
+++ b/src/widgets/homeassistant/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/homeassistant/proxy.js b/src/widgets/homeassistant/proxy.js
index e1f02ddb..7702eb5d 100644
--- a/src/widgets/homeassistant/proxy.js
+++ b/src/widgets/homeassistant/proxy.js
@@ -1,6 +1,6 @@
-import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { httpProxy } from "utils/proxy/http";
const logger = createLogger("homeassistantProxyHandler");
diff --git a/src/widgets/homebox/component.jsx b/src/widgets/homebox/component.jsx
index 18ea520e..4c550748 100644
--- a/src/widgets/homebox/component.jsx
+++ b/src/widgets/homebox/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 const homeboxDefaultFields = ["items", "locations", "totalValue"];
diff --git a/src/widgets/homebox/proxy.js b/src/widgets/homebox/proxy.js
index c91ce552..8a4550fc 100644
--- a/src/widgets/homebox/proxy.js
+++ b/src/widgets/homebox/proxy.js
@@ -1,9 +1,9 @@
import cache from "memory-cache";
-import { formatApiCall } from "utils/proxy/api-helpers";
-import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
const proxyName = "homeboxProxyHandler";
const sessionTokenCacheKey = `${proxyName}__sessionToken`;
diff --git a/src/widgets/homebridge/component.jsx b/src/widgets/homebridge/component.jsx
index 3237423e..6201b70b 100644
--- a/src/widgets/homebridge/component.jsx
+++ b/src/widgets/homebridge/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js
index 4da9197b..675e2976 100644
--- a/src/widgets/homebridge/proxy.js
+++ b/src/widgets/homebridge/proxy.js
@@ -1,9 +1,9 @@
import cache from "memory-cache";
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const proxyName = "homebridgeProxyHandler";
diff --git a/src/widgets/iframe/component.jsx b/src/widgets/iframe/component.jsx
index 21075dbe..9d2d60ca 100644
--- a/src/widgets/iframe/component.jsx
+++ b/src/widgets/iframe/component.jsx
@@ -1,7 +1,6 @@
-import { useState, useEffect } from "react";
import classNames from "classnames";
-
import Container from "components/services/widget/container";
+import { useEffect, useState } from "react";
export default function Component({ service }) {
const [refreshTimer, setRefreshTimer] = useState(0);
@@ -25,7 +24,7 @@ export default function Component({ service }) {
@@ -42,7 +41,7 @@ export default function Component({ service }) {
style={{
scrollingDisableStyle,
}}
- className={`rounded w-full ${classes}`}
+ className={`rounded-sm w-full ${classes}`}
/>
diff --git a/src/widgets/immich/component.jsx b/src/widgets/immich/component.jsx
index ed27d4d8..a38cac1e 100644
--- a/src/widgets/immich/component.jsx
+++ b/src/widgets/immich/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/jackett/component.jsx b/src/widgets/jackett/component.jsx
index 122e5012..63e3e1c3 100644
--- a/src/widgets/jackett/component.jsx
+++ b/src/widgets/jackett/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/jackett/proxy.js b/src/widgets/jackett/proxy.js
index 035309b3..10e85175 100644
--- a/src/widgets/jackett/proxy.js
+++ b/src/widgets/jackett/proxy.js
@@ -1,7 +1,7 @@
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const logger = createLogger("jackettProxyHandler");
diff --git a/src/widgets/jdownloader/component.jsx b/src/widgets/jdownloader/component.jsx
index 0e7ef72e..a7722c7c 100644
--- a/src/widgets/jdownloader/component.jsx
+++ b/src/widgets/jdownloader/component.jsx
@@ -1,7 +1,7 @@
-import { useTranslation } from "next-i18next";
-
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";
export default function Component({ service }) {
diff --git a/src/widgets/jdownloader/proxy.js b/src/widgets/jdownloader/proxy.js
index ae8c845c..d5d5ac3d 100644
--- a/src/widgets/jdownloader/proxy.js
+++ b/src/widgets/jdownloader/proxy.js
@@ -2,11 +2,11 @@
import crypto from "crypto";
import querystring from "querystring";
-import { sha256, uniqueRid, validateRid, createEncryptionToken, decrypt, encrypt } from "./tools";
+import { createEncryptionToken, decrypt, encrypt, sha256, uniqueRid, validateRid } from "./tools";
import getServiceWidget from "utils/config/service-helpers";
-import { httpProxy } from "utils/proxy/http";
import createLogger from "utils/logger";
+import { httpProxy } from "utils/proxy/http";
const proxyName = "jdownloaderProxyHandler";
const logger = createLogger(proxyName);
diff --git a/src/widgets/jellyseerr/component.jsx b/src/widgets/jellyseerr/component.jsx
index 7fb3971e..d99fffdb 100644
--- a/src/widgets/jellyseerr/component.jsx
+++ b/src/widgets/jellyseerr/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/karakeep/component.jsx b/src/widgets/karakeep/component.jsx
new file mode 100644
index 00000000..8a74662d
--- /dev/null
+++ b/src/widgets/karakeep/component.jsx
@@ -0,0 +1,49 @@
+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";
+
+export const karakeepDefaultFields = ["bookmarks", "favorites", "archived", "highlights"];
+const MAX_ALLOWED_FIELDS = 4;
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+ const { widget } = service;
+
+ const { data: statsData, error: statsError } = useWidgetAPI(widget, "stats");
+
+ if (statsError) {
+ return ;
+ }
+
+ if (!widget.fields || widget.fields.length === 0) {
+ widget.fields = karakeepDefaultFields;
+ } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
+ widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
+ }
+
+ if (!statsData) {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/karakeep/widget.js b/src/widgets/karakeep/widget.js
new file mode 100644
index 00000000..b005b04f
--- /dev/null
+++ b/src/widgets/karakeep/widget.js
@@ -0,0 +1,14 @@
+import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
+
+const widget = {
+ api: `{url}/api/v1/{endpoint}`,
+ proxyHandler: credentialedProxyHandler,
+
+ mappings: {
+ stats: {
+ endpoint: "users/me/stats",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/kavita/component.jsx b/src/widgets/kavita/component.jsx
index d2d8f442..887b3bbe 100644
--- a/src/widgets/kavita/component.jsx
+++ b/src/widgets/kavita/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/kavita/proxy.js b/src/widgets/kavita/proxy.js
index 1c41c45f..cb3b3569 100644
--- a/src/widgets/kavita/proxy.js
+++ b/src/widgets/kavita/proxy.js
@@ -1,9 +1,9 @@
import cache from "memory-cache";
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const proxyName = "kavitaProxyHandler";
@@ -14,7 +14,17 @@ async function login(widget, service) {
const endpoint = "Account/login";
const api = widgets?.[widget.type]?.api;
const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
- const loginBody = { username: widget.username, password: widget.password };
+ const loginBody = {
+ username: "",
+ password: "",
+ apiKey: "",
+ };
+ if (widget.username && widget.password) {
+ loginBody.username = widget.username;
+ loginBody.password = widget.password;
+ } else if (widget.key) {
+ loginBody.apiKey = widget.key;
+ }
const headers = { "Content-Type": "application/json", accept: "text/plain" };
const [, , data] = await httpProxy(loginUrl, {
diff --git a/src/widgets/komga/component.jsx b/src/widgets/komga/component.jsx
index 3cf51bd8..1e31b726 100644
--- a/src/widgets/komga/component.jsx
+++ b/src/widgets/komga/component.jsx
@@ -1,23 +1,20 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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: libraryData, error: libraryError } = useWidgetAPI(widget, "libraries");
- const { data: seriesData, error: seriesError } = useWidgetAPI(widget, "series");
- const { data: bookData, error: bookError } = useWidgetAPI(widget, "books");
+ const { data: komgaData, error: komgaError } = useWidgetAPI(widget);
- if (libraryError || seriesError || bookError) {
- const finalError = libraryError ?? seriesError ?? bookError;
- return ;
+ if (komgaError) {
+ return ;
}
- if (!libraryData || !seriesData || !bookData) {
+ if (!komgaData) {
return (
@@ -27,9 +24,11 @@ export default function Component({ service }) {
);
}
+ const { libraries: libraryData, series: seriesData, books: bookData } = komgaData;
+
return (
-
+
diff --git a/src/widgets/komga/proxy.js b/src/widgets/komga/proxy.js
new file mode 100644
index 00000000..b3d72690
--- /dev/null
+++ b/src/widgets/komga/proxy.js
@@ -0,0 +1,86 @@
+import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
+import widgets from "widgets/widgets";
+
+const proxyName = "komgaProxyHandler";
+const logger = createLogger(proxyName);
+
+export default async function komgaProxyHandler(req, res) {
+ const { group, service, 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) {
+ try {
+ const data = {};
+ const headers = {
+ accept: "application/json",
+ "Content-Type": "application/json",
+ };
+ if (widget.username && widget.password) {
+ headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
+ } else if (widget.key) {
+ headers["X-API-Key"] = widget.key;
+ }
+ const librariesURL = formatApiCall(widgets?.[widget.type].api, { ...widget, endpoint: "libraries" });
+ const [librariesStatus, , librariesData] = await httpProxy(librariesURL, {
+ method: "GET",
+ headers,
+ cookieHeader: "X-Auth-Token",
+ });
+
+ if (librariesStatus !== 200) {
+ return res.status(librariesStatus).send(data);
+ }
+
+ data.libraries = JSON.parse(Buffer.from(librariesData).toString()).filter((library) => !library.unavailable);
+
+ const seriesEndpointName = widget.version === 2 ? "seriesv2" : "series";
+ const seriesEndpoint = widgets[widget.type].mappings[seriesEndpointName].endpoint;
+ const seriesURL = formatApiCall(widgets?.[widget.type].api, { ...widget, endpoint: seriesEndpoint });
+ const [seriesStatus, , seriesData] = await httpProxy(seriesURL, {
+ method: widgets[widget.type].mappings[seriesEndpointName].method || "GET",
+ headers,
+ body: "{}",
+ cookieHeader: "X-Auth-Token",
+ });
+
+ if (seriesStatus !== 200) {
+ return res.status(seriesStatus).send(data);
+ }
+
+ data.series = JSON.parse(Buffer.from(seriesData).toString());
+
+ const booksEndpointName = widget.version === 2 ? "booksv2" : "books";
+ const booksEndpoint = widgets[widget.type].mappings[booksEndpointName].endpoint;
+ const booksURL = formatApiCall(widgets?.[widget.type].api, { ...widget, endpoint: booksEndpoint });
+ const [booksStatus, , booksData] = await httpProxy(booksURL, {
+ method: widgets[widget.type].mappings[booksEndpointName].method || "GET",
+ headers,
+ body: "{}",
+ cookieHeader: "X-Auth-Token",
+ });
+
+ if (booksStatus !== 200) {
+ return res.status(booksStatus).send(data);
+ }
+
+ data.books = JSON.parse(Buffer.from(booksData).toString());
+
+ return res.send(data);
+ } catch (e) {
+ logger.error("Error communicating with Komga API: %s", e);
+ return res.status(500).json({ error: "Error communicating with Komga API" });
+ }
+ }
+ }
+
+ return res.status(400).json({ error: "Invalid proxy service type" });
+}
diff --git a/src/widgets/komga/widget.js b/src/widgets/komga/widget.js
index ee01e391..e89bd746 100644
--- a/src/widgets/komga/widget.js
+++ b/src/widgets/komga/widget.js
@@ -1,25 +1,31 @@
-import genericProxyHandler from "utils/proxy/handlers/generic";
-import { jsonArrayFilter } from "utils/proxy/api-helpers";
+import komgaProxyHandler from "./proxy";
const widget = {
api: "{url}/api/v1/{endpoint}",
- proxyHandler: genericProxyHandler,
+ proxyHandler: komgaProxyHandler,
mappings: {
libraries: {
endpoint: "libraries",
- map: (data) => ({
- total: jsonArrayFilter(data, (item) => !item.unavailable).length,
- }),
},
series: {
endpoint: "series",
validate: ["totalElements"],
},
+ seriesv2: {
+ endpoint: "series/list",
+ method: "POST",
+ validate: ["totalElements"],
+ },
books: {
endpoint: "books",
validate: ["totalElements"],
},
+ booksv2: {
+ endpoint: "books/list",
+ method: "POST",
+ validate: ["totalElements"],
+ },
},
};
diff --git a/src/widgets/kopia/component.jsx b/src/widgets/kopia/component.jsx
index 2e361960..d022a545 100755
--- a/src/widgets/kopia/component.jsx
+++ b/src/widgets/kopia/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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";
function relativeDate(date) {
diff --git a/src/widgets/kubernetes/component.jsx b/src/widgets/kubernetes/component.jsx
index 68d0da29..d3587a59 100644
--- a/src/widgets/kubernetes/component.jsx
+++ b/src/widgets/kubernetes/component.jsx
@@ -1,8 +1,7 @@
-import useSWR from "swr";
-import { useTranslation } from "next-i18next";
-
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+import { useTranslation } from "next-i18next";
+import useSWR from "swr";
export default function Component({ service }) {
const { t } = useTranslation();
diff --git a/src/widgets/lidarr/component.jsx b/src/widgets/lidarr/component.jsx
index 68360d82..92f5b893 100644
--- a/src/widgets/lidarr/component.jsx
+++ b/src/widgets/lidarr/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/linkwarden/component.jsx b/src/widgets/linkwarden/component.jsx
index e74a90a8..b2b0d91a 100644
--- a/src/widgets/linkwarden/component.jsx
+++ b/src/widgets/linkwarden/component.jsx
@@ -1,7 +1,7 @@
-import React, { useState, useEffect } from "react";
-
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+import { useEffect, useState } from "react";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/lubelogger/component.jsx b/src/widgets/lubelogger/component.jsx
index 48995073..5ec88983 100644
--- a/src/widgets/lubelogger/component.jsx
+++ b/src/widgets/lubelogger/component.jsx
@@ -1,7 +1,7 @@
-import { useTranslation } from "react-i18next";
-
-import Container from "components/services/widget/container";
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";
export default function Component({ service }) {
diff --git a/src/widgets/mailcow/component.jsx b/src/widgets/mailcow/component.jsx
index 5d5fee9d..e5d9db65 100644
--- a/src/widgets/mailcow/component.jsx
+++ b/src/widgets/mailcow/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/mastodon/component.jsx b/src/widgets/mastodon/component.jsx
index 154c9d17..3a5d9ab6 100644
--- a/src/widgets/mastodon/component.jsx
+++ b/src/widgets/mastodon/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/mealie/component.jsx b/src/widgets/mealie/component.jsx
index a4dd1bf1..4a558157 100644
--- a/src/widgets/mealie/component.jsx
+++ b/src/widgets/mealie/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/medusa/component.jsx b/src/widgets/medusa/component.jsx
index d7cc3f39..88f55bcb 100644
--- a/src/widgets/medusa/component.jsx
+++ b/src/widgets/medusa/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/medusa/widget.js b/src/widgets/medusa/widget.js
index 3619d16b..fbfd8af2 100644
--- a/src/widgets/medusa/widget.js
+++ b/src/widgets/medusa/widget.js
@@ -1,7 +1,7 @@
import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
- api: "{url}/api/v1/{key}/{endpoint}/",
+ api: "{url}/api/v1/{key}/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
diff --git a/src/widgets/mikrotik/component.jsx b/src/widgets/mikrotik/component.jsx
index 74a9c9ed..4bab6792 100644
--- a/src/widgets/mikrotik/component.jsx
+++ b/src/widgets/mikrotik/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/minecraft/component.jsx b/src/widgets/minecraft/component.jsx
index 671e21ab..00c5f6f8 100644
--- a/src/widgets/minecraft/component.jsx
+++ b/src/widgets/minecraft/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/minecraft/proxy.js b/src/widgets/minecraft/proxy.js
index 98d1be88..d1fe1463 100644
--- a/src/widgets/minecraft/proxy.js
+++ b/src/widgets/minecraft/proxy.js
@@ -1,7 +1,7 @@
-import { pingWithPromise } from "minecraft-ping-js";
+import mc from "minecraftstatuspinger";
-import createLogger from "utils/logger";
import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
const proxyName = "minecraftProxyHandler";
const logger = createLogger(proxyName);
@@ -11,11 +11,14 @@ export default async function minecraftProxyHandler(req, res) {
const serviceWidget = await getServiceWidget(group, service, index);
const url = new URL(serviceWidget.url);
try {
- const pingResponse = await pingWithPromise(url.hostname, url.port || 25565);
+ const pingResponse = await mc.lookup({
+ host: url.hostname,
+ port: url.port || 25565,
+ });
res.status(200).send({
- version: pingResponse.version.name,
+ version: pingResponse.status.version.name,
online: true,
- players: pingResponse.players,
+ players: pingResponse.status.players,
});
} catch (e) {
if (e) logger.error(e);
diff --git a/src/widgets/miniflux/component.jsx b/src/widgets/miniflux/component.jsx
index aa4699ad..2cbbb254 100644
--- a/src/widgets/miniflux/component.jsx
+++ b/src/widgets/miniflux/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/moonraker/component.jsx b/src/widgets/moonraker/component.jsx
index 78332135..238c8327 100644
--- a/src/widgets/moonraker/component.jsx
+++ b/src/widgets/moonraker/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
@@ -36,14 +36,16 @@ export default function Component({ service }) {
const printStatsInfo = printStats.result.status.print_stats.info ?? {};
const { current_layer: currentLayer = "-", total_layer: totalLayer = "-" } = printStatsInfo;
+ const layers = printStats.result.status.print_stats.state === "standby" ? "- / -" : `${currentLayer} / ${totalLayer}`;
+ const progress =
+ printStats.result.status.print_stats.state === "standby"
+ ? "-"
+ : t("common.percent", { value: displayStatus.result.status.display_status.progress * 100 });
return (
-
-
+
+
);
diff --git a/src/widgets/mylar/component.jsx b/src/widgets/mylar/component.jsx
index 2d080ac0..95ec1aca 100644
--- a/src/widgets/mylar/component.jsx
+++ b/src/widgets/mylar/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/myspeed/component.jsx b/src/widgets/myspeed/component.jsx
index e4c481c7..dacd2b59 100644
--- a/src/widgets/myspeed/component.jsx
+++ b/src/widgets/myspeed/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/navidrome/component.jsx b/src/widgets/navidrome/component.jsx
index 367ca985..6218e6d9 100644
--- a/src/widgets/navidrome/component.jsx
+++ b/src/widgets/navidrome/component.jsx
@@ -1,6 +1,6 @@
+import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
-import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function SinglePlayingEntry({ entry }) {
diff --git a/src/widgets/netalertx/component.jsx b/src/widgets/netalertx/component.jsx
index 5172121e..786db9a5 100644
--- a/src/widgets/netalertx/component.jsx
+++ b/src/widgets/netalertx/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/netdata/component.jsx b/src/widgets/netdata/component.jsx
index 9d7f2469..2d04aa23 100644
--- a/src/widgets/netdata/component.jsx
+++ b/src/widgets/netdata/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/nextcloud/component.jsx b/src/widgets/nextcloud/component.jsx
index 4e673e4a..d1f1cac9 100755
--- a/src/widgets/nextcloud/component.jsx
+++ b/src/widgets/nextcloud/component.jsx
@@ -1,8 +1,8 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
import { useMemo } from "react";
-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 }) {
diff --git a/src/widgets/nextdns/component.jsx b/src/widgets/nextdns/component.jsx
index 79afc5cd..45e01281 100644
--- a/src/widgets/nextdns/component.jsx
+++ b/src/widgets/nextdns/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/npm/component.jsx b/src/widgets/npm/component.jsx
index e9f7e871..54e123a8 100644
--- a/src/widgets/npm/component.jsx
+++ b/src/widgets/npm/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/npm/proxy.js b/src/widgets/npm/proxy.js
index 6c7ba09e..79307782 100644
--- a/src/widgets/npm/proxy.js
+++ b/src/widgets/npm/proxy.js
@@ -1,10 +1,10 @@
import cache from "memory-cache";
import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
-import createLogger from "utils/logger";
const proxyName = "npmProxyHandler";
const tokenCacheKey = `${proxyName}__token`;
diff --git a/src/widgets/nzbget/component.jsx b/src/widgets/nzbget/component.jsx
index 1c38abf7..a11ac9da 100644
--- a/src/widgets/nzbget/component.jsx
+++ b/src/widgets/nzbget/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/octoprint/component.jsx b/src/widgets/octoprint/component.jsx
index a42475bd..e6b13809 100644
--- a/src/widgets/octoprint/component.jsx
+++ b/src/widgets/octoprint/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/omada/component.jsx b/src/widgets/omada/component.jsx
index e63f93d3..bf301d81 100644
--- a/src/widgets/omada/component.jsx
+++ b/src/widgets/omada/component.jsx
@@ -1,8 +1,8 @@
import { useTranslation } from "next-i18next";
-import useWidgetAPI from "../../utils/proxy/use-widget-api";
-import Container from "../../components/services/widget/container";
import Block from "../../components/services/widget/block";
+import Container from "../../components/services/widget/container";
+import useWidgetAPI from "../../utils/proxy/use-widget-api";
export default function Component({ service }) {
const { t } = useTranslation();
@@ -18,7 +18,7 @@ export default function Component({ service }) {
}
if (!widget.fields) {
- widget.fields = ["connectedAp", "activeUser", "alerts", "connectedGateway"];
+ widget.fields = ["connectedAp", "activeUser", "alerts", "connectedGateways"];
} else if (widget.fields?.length > 4) {
widget.fields = widget.fields.slice(0, 4);
}
@@ -29,7 +29,7 @@ export default function Component({ service }) {
-
+
);
@@ -40,7 +40,7 @@ export default function Component({ service }) {
-
+
);
diff --git a/src/widgets/omada/proxy.js b/src/widgets/omada/proxy.js
index f4da1293..a5af47b2 100644
--- a/src/widgets/omada/proxy.js
+++ b/src/widgets/omada/proxy.js
@@ -1,6 +1,6 @@
-import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { httpProxy } from "utils/proxy/http";
const proxyName = "omadaProxyHandler";
diff --git a/src/widgets/ombi/component.jsx b/src/widgets/ombi/component.jsx
index 0d31081a..859c01aa 100644
--- a/src/widgets/ombi/component.jsx
+++ b/src/widgets/ombi/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/opendtu/component.jsx b/src/widgets/opendtu/component.jsx
index e91495c5..c1b924b9 100644
--- a/src/widgets/opendtu/component.jsx
+++ b/src/widgets/opendtu/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/openmediavault/component.jsx b/src/widgets/openmediavault/component.jsx
index bd34a750..c14581f9 100644
--- a/src/widgets/openmediavault/component.jsx
+++ b/src/widgets/openmediavault/component.jsx
@@ -1,6 +1,6 @@
+import DownloaderGetDownloadList from "./methods/downloader_get_downloadlist";
import ServicesGetStatus from "./methods/services_get_status";
import SmartGetList from "./methods/smart_get_list";
-import DownloaderGetDownloadList from "./methods/downloader_get_downloadlist";
export default function Component({ service }) {
switch (service.widget.method) {
diff --git a/src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx b/src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx
index ed776db0..7caf3426 100644
--- a/src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx
+++ b/src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx
@@ -1,6 +1,7 @@
-import useWidgetAPI from "utils/proxy/use-widget-api";
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
+import useWidgetAPI from "utils/proxy/use-widget-api";
const downloadReduce = (acc, e) => {
if (e.downloading) {
diff --git a/src/widgets/openmediavault/methods/services_get_status.jsx b/src/widgets/openmediavault/methods/services_get_status.jsx
index 3ec66a45..7ab0f8c1 100644
--- a/src/widgets/openmediavault/methods/services_get_status.jsx
+++ b/src/widgets/openmediavault/methods/services_get_status.jsx
@@ -1,6 +1,7 @@
-import useWidgetAPI from "utils/proxy/use-widget-api";
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
+import useWidgetAPI from "utils/proxy/use-widget-api";
const isRunningReduce = (acc, e) => {
if (e.running) {
diff --git a/src/widgets/openmediavault/methods/smart_get_list.jsx b/src/widgets/openmediavault/methods/smart_get_list.jsx
index b8ca33ee..4998c02f 100644
--- a/src/widgets/openmediavault/methods/smart_get_list.jsx
+++ b/src/widgets/openmediavault/methods/smart_get_list.jsx
@@ -1,6 +1,7 @@
-import useWidgetAPI from "utils/proxy/use-widget-api";
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
+import useWidgetAPI from "utils/proxy/use-widget-api";
const passedReduce = (acc, e) => {
if (e.monitor && e.overallstatus === "GOOD") {
diff --git a/src/widgets/openmediavault/proxy.js b/src/widgets/openmediavault/proxy.js
index 9cda42e8..d2365f2b 100644
--- a/src/widgets/openmediavault/proxy.js
+++ b/src/widgets/openmediavault/proxy.js
@@ -1,8 +1,8 @@
-import { formatApiCall } from "utils/proxy/api-helpers";
-import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
-import { addCookieToJar, setCookieHeader } from "utils/proxy/cookie-jar";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { addCookieToJar, setCookieHeader } from "utils/proxy/cookie-jar";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const PROXY_NAME = "OMVProxyHandler";
diff --git a/src/widgets/openwrt/methods/interface.jsx b/src/widgets/openwrt/methods/interface.jsx
index 91366ec9..4f77036f 100644
--- a/src/widgets/openwrt/methods/interface.jsx
+++ b/src/widgets/openwrt/methods/interface.jsx
@@ -1,8 +1,8 @@
+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";
-import Container from "components/services/widget/container";
-import Block from "components/services/widget/block";
export default function Component({ service }) {
const { t } = useTranslation();
diff --git a/src/widgets/openwrt/methods/system.jsx b/src/widgets/openwrt/methods/system.jsx
index 0a8146cc..55c0b005 100644
--- a/src/widgets/openwrt/methods/system.jsx
+++ b/src/widgets/openwrt/methods/system.jsx
@@ -1,8 +1,8 @@
+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";
-import Container from "components/services/widget/container";
-import Block from "components/services/widget/block";
export default function Component({ service }) {
const { t } = useTranslation();
diff --git a/src/widgets/openwrt/proxy.js b/src/widgets/openwrt/proxy.js
index 0a0da3ff..5db90790 100644
--- a/src/widgets/openwrt/proxy.js
+++ b/src/widgets/openwrt/proxy.js
@@ -1,7 +1,7 @@
-import { sendJsonRpcRequest } from "utils/proxy/handlers/jsonrpc";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { sendJsonRpcRequest } from "utils/proxy/handlers/jsonrpc";
import widgets from "widgets/widgets";
const PROXY_NAME = "OpenWRTProxyHandler";
diff --git a/src/widgets/opnsense/component.jsx b/src/widgets/opnsense/component.jsx
index 1b2171a8..1caaab47 100644
--- a/src/widgets/opnsense/component.jsx
+++ b/src/widgets/opnsense/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/overseerr/component.jsx b/src/widgets/overseerr/component.jsx
index 336bc6fc..d5c64392 100644
--- a/src/widgets/overseerr/component.jsx
+++ b/src/widgets/overseerr/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/paperlessngx/component.jsx b/src/widgets/paperlessngx/component.jsx
index cd9cc559..bafc2e99 100644
--- a/src/widgets/paperlessngx/component.jsx
+++ b/src/widgets/paperlessngx/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/peanut/component.jsx b/src/widgets/peanut/component.jsx
index aa6f31b3..54a293ad 100644
--- a/src/widgets/peanut/component.jsx
+++ b/src/widgets/peanut/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/pfsense/component.jsx b/src/widgets/pfsense/component.jsx
index 8e02c590..9f43488b 100644
--- a/src/widgets/pfsense/component.jsx
+++ b/src/widgets/pfsense/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/photoprism/component.jsx b/src/widgets/photoprism/component.jsx
index e5033eda..21817a10 100644
--- a/src/widgets/photoprism/component.jsx
+++ b/src/widgets/photoprism/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/photoprism/proxy.js b/src/widgets/photoprism/proxy.js
index fe5096b3..1959817a 100644
--- a/src/widgets/photoprism/proxy.js
+++ b/src/widgets/photoprism/proxy.js
@@ -1,7 +1,7 @@
-import { formatApiCall } from "utils/proxy/api-helpers";
-import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
const logger = createLogger("photoprismProxyHandler");
diff --git a/src/widgets/pihole/component.jsx b/src/widgets/pihole/component.jsx
index 7aa706e4..6895ab28 100644
--- a/src/widgets/pihole/component.jsx
+++ b/src/widgets/pihole/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/pihole/proxy.js b/src/widgets/pihole/proxy.js
index bf24624d..75cd0fb5 100644
--- a/src/widgets/pihole/proxy.js
+++ b/src/widgets/pihole/proxy.js
@@ -1,9 +1,9 @@
import cache from "memory-cache";
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const proxyName = "piholeProxyHandler";
@@ -28,7 +28,11 @@ async function login(widget, service) {
logger.error("Failed to login to Pi-Hole API, status: %d", status);
cache.del(`${sessionSIDCacheKey}.${service}`);
} else {
- cache.put(`${sessionSIDCacheKey}.${service}`, dataParsed.session.sid, dataParsed.session.validity);
+ cache.put(
+ `${sessionSIDCacheKey}.${service}`,
+ dataParsed.session.sid,
+ Math.min(2147483647, dataParsed.session.validity * 1000), // https://github.com/ptarjan/node-cache/issues/84
+ );
}
}
diff --git a/src/widgets/plantit/component.jsx b/src/widgets/plantit/component.jsx
index ea203b87..d93304e7 100644
--- a/src/widgets/plantit/component.jsx
+++ b/src/widgets/plantit/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/plex/component.jsx b/src/widgets/plex/component.jsx
index 86ba5503..153d57b5 100644
--- a/src/widgets/plex/component.jsx
+++ b/src/widgets/plex/component.jsx
@@ -1,7 +1,7 @@
-import { useTranslation } from "next-i18next";
-
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";
export default function Component({ service }) {
diff --git a/src/widgets/plex/proxy.js b/src/widgets/plex/proxy.js
index 2956f280..18ffc49b 100644
--- a/src/widgets/plex/proxy.js
+++ b/src/widgets/plex/proxy.js
@@ -2,10 +2,10 @@
import cache from "memory-cache";
import { xml2json } from "xml-js";
-import { formatApiCall } from "utils/proxy/api-helpers";
-import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const proxyName = "plexProxyHandler";
@@ -41,7 +41,12 @@ async function fetchFromPlexAPI(endpoint, widget) {
const url = new URL(formatApiCall(api, { endpoint, ...widget }));
- const [status, contentType, data] = await httpProxy(url);
+ const [status, contentType, data] = await httpProxy(url, {
+ headers: {
+ "X-Plex-Container-Start": `0`,
+ "X-Plex-Container-Size": `500`,
+ },
+ });
if (status !== 200) {
logger.error("HTTP %d communicating with Plex. Data: %s", status, data.toString());
@@ -106,7 +111,8 @@ export default async function plexProxyHandler(req, res) {
: `/library/sections/${library._attributes.key}/albums`; // music
[status, apiData] = await fetchFromPlexAPI(libraryURL, widget);
if (apiData && apiData.MediaContainer) {
- const size = parseInt(apiData.MediaContainer._attributes.size, 10);
+ const sizeProp = apiData.MediaContainer._attributes["totalSize"] ? "totalSize" : "size";
+ const size = parseInt(apiData.MediaContainer._attributes[sizeProp], 10);
if (library._attributes.type === "movie") {
movies += size;
} else if (library._attributes.type === "show") {
diff --git a/src/widgets/portainer/component.jsx b/src/widgets/portainer/component.jsx
index 41d541dc..f8a89507 100644
--- a/src/widgets/portainer/component.jsx
+++ b/src/widgets/portainer/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/prometheus/component.jsx b/src/widgets/prometheus/component.jsx
index da93d490..7b3722d0 100644
--- a/src/widgets/prometheus/component.jsx
+++ b/src/widgets/prometheus/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/prometheus/widget.js b/src/widgets/prometheus/widget.js
index 1ecf7b7c..817dc029 100644
--- a/src/widgets/prometheus/widget.js
+++ b/src/widgets/prometheus/widget.js
@@ -6,7 +6,7 @@ const widget = {
mappings: {
targets: {
- endpoint: "targets",
+ endpoint: "targets?state=active",
validate: ["data"],
},
},
diff --git a/src/widgets/prometheusmetric/component.jsx b/src/widgets/prometheusmetric/component.jsx
index 350a6b7d..d58efde9 100644
--- a/src/widgets/prometheusmetric/component.jsx
+++ b/src/widgets/prometheusmetric/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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";
function formatValue(t, metric, rawValue) {
diff --git a/src/widgets/prowlarr/component.jsx b/src/widgets/prowlarr/component.jsx
index 6f7211f0..e454c1a7 100644
--- a/src/widgets/prowlarr/component.jsx
+++ b/src/widgets/prowlarr/component.jsx
@@ -1,7 +1,7 @@
-import { useTranslation } from "react-i18next";
-
-import Container from "components/services/widget/container";
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";
export default function Component({ service }) {
diff --git a/src/widgets/proxmox/component.jsx b/src/widgets/proxmox/component.jsx
index f7493e7a..51762a73 100644
--- a/src/widgets/proxmox/component.jsx
+++ b/src/widgets/proxmox/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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";
function calcRunning(total, current) {
diff --git a/src/widgets/proxmoxbackupserver/component.jsx b/src/widgets/proxmoxbackupserver/component.jsx
index d6320d61..b13f8756 100644
--- a/src/widgets/proxmoxbackupserver/component.jsx
+++ b/src/widgets/proxmoxbackupserver/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
@@ -29,7 +29,18 @@ export default function Component({ service }) {
);
}
- const datastoreUsage = datastoreData.data ? (datastoreData.data[0].used / datastoreData.data[0].total) * 100 : 0;
+ const datastoreIndex = !!widget.datastore
+ ? datastoreData.data.findIndex(function (ds) {
+ return ds.store == widget.datastore;
+ })
+ : -1;
+ const datastoreUsage =
+ datastoreIndex > -1
+ ? (datastoreData.data[datastoreIndex].used / datastoreData.data[datastoreIndex].total) * 100
+ : (datastoreData.data.reduce((sum, datastore) => sum + datastore.used, 0) /
+ datastoreData.data.reduce((sum, datastore) => sum + datastore.total, 0)) *
+ 100;
+
const cpuUsage = hostData.data.cpu * 100;
const memoryUsage = (hostData.data.memory.used / hostData.data.memory.total) * 100;
const failedTasks = tasksData.total >= 100 ? "99+" : tasksData.total;
diff --git a/src/widgets/pterodactyl/component.jsx b/src/widgets/pterodactyl/component.jsx
index bd3242cc..9a702eef 100644
--- a/src/widgets/pterodactyl/component.jsx
+++ b/src/widgets/pterodactyl/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/pyload/component.jsx b/src/widgets/pyload/component.jsx
index 3df11c8e..f618f75e 100644
--- a/src/widgets/pyload/component.jsx
+++ b/src/widgets/pyload/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/pyload/proxy.js b/src/widgets/pyload/proxy.js
index a380c865..2a1949c1 100644
--- a/src/widgets/pyload/proxy.js
+++ b/src/widgets/pyload/proxy.js
@@ -1,10 +1,10 @@
import cache from "memory-cache";
import getServiceWidget from "utils/config/service-helpers";
-import { formatApiCall } from "utils/proxy/api-helpers";
-import widgets from "widgets/widgets";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
+import widgets from "widgets/widgets";
const proxyName = "pyloadProxyHandler";
const logger = createLogger(proxyName);
diff --git a/src/widgets/qbittorrent/component.jsx b/src/widgets/qbittorrent/component.jsx
index d7fb8bf7..c9f64816 100644
--- a/src/widgets/qbittorrent/component.jsx
+++ b/src/widgets/qbittorrent/component.jsx
@@ -1,9 +1,9 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
import QueueEntry from "../../components/widgets/queue/queueEntry";
-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 }) {
diff --git a/src/widgets/qbittorrent/proxy.js b/src/widgets/qbittorrent/proxy.js
index aead7582..8f1874bf 100644
--- a/src/widgets/qbittorrent/proxy.js
+++ b/src/widgets/qbittorrent/proxy.js
@@ -1,7 +1,7 @@
-import { formatApiCall } from "utils/proxy/api-helpers";
-import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
const logger = createLogger("qbittorrentProxyHandler");
diff --git a/src/widgets/qnap/component.jsx b/src/widgets/qnap/component.jsx
index 1bfa1f9a..d7fdf8bc 100644
--- a/src/widgets/qnap/component.jsx
+++ b/src/widgets/qnap/component.jsx
@@ -1,9 +1,9 @@
/* eslint no-underscore-dangle: ["error", { "allow": ["_text", "_cdata"] }] */
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/qnap/proxy.js b/src/widgets/qnap/proxy.js
index 07917d28..1c5356ae 100644
--- a/src/widgets/qnap/proxy.js
+++ b/src/widgets/qnap/proxy.js
@@ -3,10 +3,10 @@
import cache from "memory-cache";
import { xml2json } from "xml-js";
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
const proxyName = "qnapProxyHandler";
const sessionTokenCacheKey = `${proxyName}__sessionToken`;
diff --git a/src/widgets/radarr/component.jsx b/src/widgets/radarr/component.jsx
index ebb00fee..bcf9301b 100644
--- a/src/widgets/radarr/component.jsx
+++ b/src/widgets/radarr/component.jsx
@@ -1,10 +1,10 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
import { useCallback } from "react";
import QueueEntry from "../../components/widgets/queue/queueEntry";
-import Container from "components/services/widget/container";
-import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
function getProgress(sizeLeft, size) {
diff --git a/src/widgets/radarr/widget.js b/src/widgets/radarr/widget.js
index 7d370378..4f71b8d9 100644
--- a/src/widgets/radarr/widget.js
+++ b/src/widgets/radarr/widget.js
@@ -1,5 +1,5 @@
-import genericProxyHandler from "utils/proxy/handlers/generic";
import { asJson, jsonArrayFilter } from "utils/proxy/api-helpers";
+import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/v3/{endpoint}?apikey={key}",
diff --git a/src/widgets/readarr/component.jsx b/src/widgets/readarr/component.jsx
index bde9715a..845b7820 100644
--- a/src/widgets/readarr/component.jsx
+++ b/src/widgets/readarr/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/readarr/widget.js b/src/widgets/readarr/widget.js
index 58cc09c4..f786f0bc 100644
--- a/src/widgets/readarr/widget.js
+++ b/src/widgets/readarr/widget.js
@@ -1,5 +1,5 @@
-import genericProxyHandler from "utils/proxy/handlers/generic";
import { jsonArrayFilter } from "utils/proxy/api-helpers";
+import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/v1/{endpoint}?apikey={key}",
diff --git a/src/widgets/romm/component.jsx b/src/widgets/romm/component.jsx
index ea9549f7..b0787fb3 100644
--- a/src/widgets/romm/component.jsx
+++ b/src/widgets/romm/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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";
const ROMM_DEFAULT_FIELDS = ["platforms", "totalRoms", "saves", "states"];
diff --git a/src/widgets/rutorrent/component.jsx b/src/widgets/rutorrent/component.jsx
index e7dc26e7..245a786c 100644
--- a/src/widgets/rutorrent/component.jsx
+++ b/src/widgets/rutorrent/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/rutorrent/proxy.js b/src/widgets/rutorrent/proxy.js
index e0ae44fe..910f2311 100644
--- a/src/widgets/rutorrent/proxy.js
+++ b/src/widgets/rutorrent/proxy.js
@@ -1,8 +1,8 @@
import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
-import { formatApiCall } from "utils/proxy/api-helpers";
-import createLogger from "utils/logger";
const logger = createLogger("rutorrentProxyHandler");
diff --git a/src/widgets/sabnzbd/component.jsx b/src/widgets/sabnzbd/component.jsx
index 260375a4..9807dd93 100644
--- a/src/widgets/sabnzbd/component.jsx
+++ b/src/widgets/sabnzbd/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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";
function fromUnits(value) {
diff --git a/src/widgets/scrutiny/component.jsx b/src/widgets/scrutiny/component.jsx
index b0ebc767..2450a95e 100644
--- a/src/widgets/scrutiny/component.jsx
+++ b/src/widgets/scrutiny/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
// @see https://github.com/AnalogJ/scrutiny/blob/d8d56f77f9e868127c4849dac74d65512db658e8/webapp/frontend/src/app/shared/device-status.pipe.ts
diff --git a/src/widgets/slskd/component.jsx b/src/widgets/slskd/component.jsx
new file mode 100644
index 00000000..40a206b6
--- /dev/null
+++ b/src/widgets/slskd/component.jsx
@@ -0,0 +1,55 @@
+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 slskdDefaultFields = ["slskStatus", "downloads", "uploads", "sharedFiles"];
+const MAX_ALLOWED_FIELDS = 4;
+
+export default function Component({ service }) {
+ const { t } = useTranslation();
+ const { widget } = service;
+
+ const { data: appData, error: appError } = useWidgetAPI(widget, "application");
+ const { data: downData, error: downError } = useWidgetAPI(widget, "downloads");
+ const { data: upData, error: upError } = useWidgetAPI(widget, "uploads");
+
+ if (appError || downError || upError) {
+ return
;
+ }
+
+ if (!widget.fields || widget.fields.length === 0) {
+ widget.fields = slskdDefaultFields;
+ } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
+ widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
+ }
+
+ if (!appData || !downData || !upData) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/widgets/slskd/widget.js b/src/widgets/slskd/widget.js
new file mode 100644
index 00000000..3eedd356
--- /dev/null
+++ b/src/widgets/slskd/widget.js
@@ -0,0 +1,20 @@
+import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
+
+const widget = {
+ api: `{url}/api/v0/{endpoint}`,
+ proxyHandler: credentialedProxyHandler,
+
+ mappings: {
+ application: {
+ endpoint: "application",
+ },
+ downloads: {
+ endpoint: "transfers/downloads",
+ },
+ uploads: {
+ endpoint: "transfers/uploads",
+ },
+ },
+};
+
+export default widget;
diff --git a/src/widgets/sonarr/component.jsx b/src/widgets/sonarr/component.jsx
index e4b63bad..19cc2c12 100644
--- a/src/widgets/sonarr/component.jsx
+++ b/src/widgets/sonarr/component.jsx
@@ -1,10 +1,10 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
import { useCallback } from "react";
import QueueEntry from "../../components/widgets/queue/queueEntry";
-import Container from "components/services/widget/container";
-import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";
function getProgress(sizeLeft, size) {
diff --git a/src/widgets/sonarr/widget.js b/src/widgets/sonarr/widget.js
index acb4a551..1fcef8eb 100644
--- a/src/widgets/sonarr/widget.js
+++ b/src/widgets/sonarr/widget.js
@@ -1,5 +1,5 @@
-import genericProxyHandler from "utils/proxy/handlers/generic";
import { asJson } from "utils/proxy/api-helpers";
+import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/v3/{endpoint}?apikey={key}",
diff --git a/src/widgets/speedtest/component.jsx b/src/widgets/speedtest/component.jsx
index 9826f776..7be00aa2 100644
--- a/src/widgets/speedtest/component.jsx
+++ b/src/widgets/speedtest/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
@@ -9,18 +9,19 @@ export default function Component({ service }) {
const { widget } = service;
- const { data: speedtestData, error: speedtestError } = useWidgetAPI(widget, "speedtest/latest");
+ const endpoint = widget.version === 2 ? "latestv2" : "latestv1";
+ const { data: speedtestData, error: speedtestError } = useWidgetAPI(widget, endpoint);
const bitratePrecision =
!widget?.bitratePrecision || Number.isNaN(widget?.bitratePrecision) || widget?.bitratePrecision < 0
? 0
: widget.bitratePrecision;
- if (speedtestError) {
- return
;
+ if (speedtestError || speedtestData?.error) {
+ return
;
}
- if (!speedtestData) {
+ if (!speedtestData?.data) {
return (
@@ -35,14 +36,14 @@ export default function Component({ service }) {
diff --git a/src/widgets/speedtest/widget.js b/src/widgets/speedtest/widget.js
index 09e0da7c..3517de3c 100644
--- a/src/widgets/speedtest/widget.js
+++ b/src/widgets/speedtest/widget.js
@@ -1,14 +1,18 @@
-import genericProxyHandler from "utils/proxy/handlers/generic";
+import genericProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/{endpoint}",
proxyHandler: genericProxyHandler,
mappings: {
- "speedtest/latest": {
+ latestv1: {
endpoint: "speedtest/latest",
validate: ["data"],
},
+ latestv2: {
+ endpoint: "v1/results/latest",
+ validate: ["data"],
+ },
},
};
diff --git a/src/widgets/spoolman/component.jsx b/src/widgets/spoolman/component.jsx
index 523ecea7..62eb3a1d 100644
--- a/src/widgets/spoolman/component.jsx
+++ b/src/widgets/spoolman/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/stash/component.jsx b/src/widgets/stash/component.jsx
index b7c259a8..965c6b59 100644
--- a/src/widgets/stash/component.jsx
+++ b/src/widgets/stash/component.jsx
@@ -1,8 +1,8 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
import { useEffect, useState } from "react";
-import Container from "components/services/widget/container";
-import Block from "components/services/widget/block";
import { formatProxyUrl } from "utils/proxy/api-helpers";
export default function Component({ service }) {
diff --git a/src/widgets/stocks/component.jsx b/src/widgets/stocks/component.jsx
index 844365cb..be471ddf 100644
--- a/src/widgets/stocks/component.jsx
+++ b/src/widgets/stocks/component.jsx
@@ -1,8 +1,8 @@
-import { useTranslation } from "next-i18next";
import classNames from "classnames";
-
-import Container from "components/services/widget/container";
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";
function MarketStatus({ service }) {
@@ -61,7 +61,7 @@ function StockItem({ service, ticker }) {
}
return (
-
+
{ticker}
0 ? "text-emerald-300" : "text-rose-300"}`}>
diff --git a/src/widgets/strelaysrv/component.jsx b/src/widgets/strelaysrv/component.jsx
index a14678d2..026e19b7 100644
--- a/src/widgets/strelaysrv/component.jsx
+++ b/src/widgets/strelaysrv/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/suwayomi/component.jsx b/src/widgets/suwayomi/component.jsx
index b7c34820..1cbd8c53 100644
--- a/src/widgets/suwayomi/component.jsx
+++ b/src/widgets/suwayomi/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/suwayomi/proxy.js b/src/widgets/suwayomi/proxy.js
index def811cc..4df55b95 100644
--- a/src/widgets/suwayomi/proxy.js
+++ b/src/widgets/suwayomi/proxy.js
@@ -1,7 +1,7 @@
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const proxyName = "suwayomiProxyHandler";
diff --git a/src/widgets/swagdashboard/component.jsx b/src/widgets/swagdashboard/component.jsx
index d4dbd494..4220e3c8 100644
--- a/src/widgets/swagdashboard/component.jsx
+++ b/src/widgets/swagdashboard/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/tailscale/component.jsx b/src/widgets/tailscale/component.jsx
index d3c937d5..b95cb016 100644
--- a/src/widgets/tailscale/component.jsx
+++ b/src/widgets/tailscale/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/tandoor/component.jsx b/src/widgets/tandoor/component.jsx
index 40d2b88e..4a02d539 100644
--- a/src/widgets/tandoor/component.jsx
+++ b/src/widgets/tandoor/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/tautulli/component.jsx b/src/widgets/tautulli/component.jsx
index ba94f143..3f6443dd 100644
--- a/src/widgets/tautulli/component.jsx
+++ b/src/widgets/tautulli/component.jsx
@@ -1,9 +1,9 @@
/* eslint-disable camelcase */
+import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
-import { BsFillPlayFill, BsPauseFill, BsCpu, BsFillCpuFill } from "react-icons/bs";
+import { BsCpu, BsFillCpuFill, BsFillPlayFill, BsPauseFill } from "react-icons/bs";
import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md";
-import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function millisecondsToTime(milliseconds) {
diff --git a/src/widgets/tdarr/component.jsx b/src/widgets/tdarr/component.jsx
index f885830f..824a56b3 100644
--- a/src/widgets/tdarr/component.jsx
+++ b/src/widgets/tdarr/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/tdarr/proxy.js b/src/widgets/tdarr/proxy.js
index 88da30fd..d6897dfc 100644
--- a/src/widgets/tdarr/proxy.js
+++ b/src/widgets/tdarr/proxy.js
@@ -1,7 +1,7 @@
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const proxyName = "tdarrProxyHandler";
diff --git a/src/widgets/technitium/component.jsx b/src/widgets/technitium/component.jsx
index d510a59d..fa221025 100644
--- a/src/widgets/technitium/component.jsx
+++ b/src/widgets/technitium/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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";
const MAX_ALLOWED_FIELDS = 4;
diff --git a/src/widgets/technitium/widget.js b/src/widgets/technitium/widget.js
index c3432a67..fc4577be 100644
--- a/src/widgets/technitium/widget.js
+++ b/src/widgets/technitium/widget.js
@@ -1,5 +1,5 @@
-import genericProxyHandler from "utils/proxy/handlers/generic";
import { asJson } from "utils/proxy/api-helpers";
+import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/api/{endpoint}?token={key}&utc=true",
diff --git a/src/widgets/traefik/component.jsx b/src/widgets/traefik/component.jsx
index 42d261e3..e4b3b46b 100644
--- a/src/widgets/traefik/component.jsx
+++ b/src/widgets/traefik/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/transmission/component.jsx b/src/widgets/transmission/component.jsx
index 98b269a5..474fe69f 100644
--- a/src/widgets/transmission/component.jsx
+++ b/src/widgets/transmission/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/transmission/proxy.js b/src/widgets/transmission/proxy.js
index 8b8049bc..b0be7bac 100644
--- a/src/widgets/transmission/proxy.js
+++ b/src/widgets/transmission/proxy.js
@@ -1,9 +1,9 @@
import cache from "memory-cache";
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const proxyName = "transmissionProxyHandler";
diff --git a/src/widgets/truenas/component.jsx b/src/widgets/truenas/component.jsx
index ccdab297..12ceef56 100644
--- a/src/widgets/truenas/component.jsx
+++ b/src/widgets/truenas/component.jsx
@@ -1,10 +1,10 @@
-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";
+import Container from "components/services/widget/container";
+import { useTranslation } from "next-i18next";
import Pool from "widgets/truenas/pool";
+import useWidgetAPI from "utils/proxy/use-widget-api";
+
export default function Component({ service }) {
const { t } = useTranslation();
@@ -12,14 +12,15 @@ export default function Component({ service }) {
const { data: alertData, error: alertError } = useWidgetAPI(widget, "alerts");
const { data: statusData, error: statusError } = useWidgetAPI(widget, "status");
- const { data: poolsData, error: poolsError } = useWidgetAPI(widget, "pools");
+ const { data: poolsData, error: poolsError } = useWidgetAPI(widget, widget?.enablePools ? "pools" : null);
+ const { data: datasetData, error: datasetError } = useWidgetAPI(widget, widget?.enablePools ? "dataset" : null);
if (alertError || statusError || poolsError) {
- const finalError = alertError ?? statusError ?? poolsError;
+ const finalError = alertError ?? statusError ?? poolsError ?? datasetError;
return ;
}
- if (!alertData || !statusData) {
+ if (!alertData || !statusData || (widget?.enablePools && (!poolsData || !datasetData))) {
return (
@@ -29,7 +30,22 @@ export default function Component({ service }) {
);
}
- const enablePools = widget?.enablePools && Array.isArray(poolsData) && poolsData.length > 0;
+ let pools = [];
+ const showPools =
+ Array.isArray(poolsData) && poolsData.length > 0 && Array.isArray(datasetData) && datasetData.length > 0;
+
+ if (showPools) {
+ pools = poolsData.map((pool) => {
+ const dataset = datasetData.find((d) => d.pool === pool.name && d.name === pool.name);
+ return {
+ id: pool.id,
+ name: pool.name,
+ healthy: pool.healthy,
+ allocated: dataset?.used.parsed ?? 0,
+ free: dataset?.available.parsed ?? 0,
+ };
+ });
+ }
return (
<>
@@ -38,19 +54,11 @@ export default function Component({ service }) {
- {enablePools &&
- poolsData
+ {showPools &&
+ pools
.sort((a, b) => a.name.localeCompare(b.name))
.map((pool) => (
-
+
))}
>
);
diff --git a/src/widgets/truenas/pool.jsx b/src/widgets/truenas/pool.jsx
index b92ecb68..7b2052ea 100644
--- a/src/widgets/truenas/pool.jsx
+++ b/src/widgets/truenas/pool.jsx
@@ -1,19 +1,9 @@
import classNames from "classnames";
-import prettyBytes from "pretty-bytes";
+import { useTranslation } from "next-i18next";
-export default function Pool({ name, free, allocated, healthy, data, nasType }) {
- let total = 0;
- if (nasType === "scale") {
- total = free + allocated;
- } else {
- allocated = 0; // eslint-disable-line no-param-reassign
- for (let i = 0; i < data.length; i += 1) {
- total += data[i].stats.size;
- allocated += data[i].stats.allocated; // eslint-disable-line no-param-reassign
- }
- }
-
- const usedPercent = Math.round((allocated / total) * 100);
+export default function Pool({ name, free, allocated, healthy }) {
+ const { t } = useTranslation();
+ const usedPercent = Math.round((allocated / (free + allocated)) * 100);
const statusColor = healthy ? "bg-green-500" : "bg-yellow-500";
return (
@@ -25,14 +15,22 @@ export default function Pool({ name, free, allocated, healthy, data, nasType })
}}
/>
-
+
- {prettyBytes(allocated)} / {prettyBytes(total)}
+ {`${t("common.bytes", {
+ value: allocated,
+ maximumFractionDigits: 1,
+ binary: true,
+ })} / ${t("common.bytes", {
+ value: free + allocated,
+ maximumFractionDigits: 1,
+ binary: true,
+ })}`}
({usedPercent}%)
diff --git a/src/widgets/truenas/widget.js b/src/widgets/truenas/widget.js
index 8e50956b..528114ed 100644
--- a/src/widgets/truenas/widget.js
+++ b/src/widgets/truenas/widget.js
@@ -1,5 +1,5 @@
-import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
import { asJson, jsonArrayFilter } from "utils/proxy/api-helpers";
+import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: "{url}/api/v2.0/{endpoint}",
@@ -23,11 +23,11 @@ const widget = {
id: entry.name,
name: entry.name,
healthy: entry.healthy,
- allocated: entry.allocated,
- free: entry.free,
- data: entry.topology?.data ?? [],
})),
},
+ dataset: {
+ endpoint: "pool/dataset",
+ },
},
};
diff --git a/src/widgets/tubearchivist/component.jsx b/src/widgets/tubearchivist/component.jsx
index c95db074..427298e4 100644
--- a/src/widgets/tubearchivist/component.jsx
+++ b/src/widgets/tubearchivist/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
@@ -14,8 +14,8 @@ export default function Component({ service }) {
const { data: channelsData, error: channelsError } = useWidgetAPI(widget, "channels");
const { data: playlistsData, error: playlistsError } = useWidgetAPI(widget, "playlists");
- if (downloadsError || videosError || channelsError || playlistsError) {
- const finalError = downloadsError ?? videosError ?? channelsError ?? playlistsError;
+ if (downloadsError || videosError || channelsError || playlistsError || (downloadsData && downloadsData.detail)) {
+ const finalError = downloadsError ?? videosError ?? channelsError ?? playlistsError ?? downloadsData.detail;
return ;
}
diff --git a/src/widgets/unifi/component.jsx b/src/widgets/unifi/component.jsx
index 2d5784b7..ad58d1d9 100644
--- a/src/widgets/unifi/component.jsx
+++ b/src/widgets/unifi/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/unifi/proxy.js b/src/widgets/unifi/proxy.js
index 559065e3..c932fc41 100644
--- a/src/widgets/unifi/proxy.js
+++ b/src/widgets/unifi/proxy.js
@@ -1,11 +1,11 @@
import cache from "memory-cache";
-import { formatApiCall } from "utils/proxy/api-helpers";
-import { httpProxy } from "utils/proxy/http";
-import { addCookieToJar, setCookieHeader } from "utils/proxy/cookie-jar";
import getServiceWidget from "utils/config/service-helpers";
import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { addCookieToJar, setCookieHeader } from "utils/proxy/cookie-jar";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const udmpPrefix = "/proxy/network";
@@ -47,7 +47,7 @@ async function login(widget, csrfToken) {
const endpoint = widget.prefix === udmpPrefix ? "auth/login" : "login";
const api = widgets?.[widget.type]?.api?.replace("{prefix}", ""); // no prefix for login url
const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget }));
- const loginBody = { username: widget.username, password: widget.password, remember: true };
+ const loginBody = { username: widget.username, password: widget.password, remember: true, rememberMe: true };
const headers = { "Content-Type": "application/json" };
if (csrfToken) {
headers["X-CSRF-TOKEN"] = csrfToken;
@@ -75,31 +75,39 @@ export default async function unifiProxyHandler(req, res) {
let [status, contentType, data, responseHeaders] = [];
let prefix = cache.get(`${prefixCacheKey}.${service}`);
let csrfToken;
- if (prefix === null) {
- // auto detect if we're talking to a UDM Pro, and cache the result so that we
- // don't make two requests each time data from Unifi is required
+ const headers = {};
+ if (widget.key) {
+ prefix = udmpPrefix;
+ headers["X-API-KEY"] = widget.key;
+ headers["Accept"] = "application/json";
+ } else if (prefix === null) {
+ // auto detect if we're talking to a UDM Pro or Network API device, and cache the result
+ // so that we don't make two requests each time data from Unifi is required
[status, contentType, data, responseHeaders] = await httpProxy(widget.url);
prefix = "";
if (responseHeaders?.["x-csrf-token"]) {
// Unifi OS < 3.2.5 passes & requires csrf-token
prefix = udmpPrefix;
csrfToken = responseHeaders["x-csrf-token"];
- } else if (responseHeaders?.["access-control-expose-headers"]) {
- // Unifi OS ≥ 3.2.5 doesnt pass csrf token but still uses different endpoint
+ } else if (
+ responseHeaders?.["access-control-expose-headers"] ||
+ responseHeaders?.["Access-Control-Expose-Headers"]
+ ) {
+ // Unifi OS ≥ 3.2.5 doesnt pass csrf token but still uses different endpoint, same with Network API
prefix = udmpPrefix;
}
- cache.put(`${prefixCacheKey}.${service}`, prefix);
}
+ cache.put(`${prefixCacheKey}.${service}`, prefix);
widget.prefix = prefix;
const { endpoint } = req.query;
const url = new URL(formatApiCall(api, { endpoint, ...widget }));
- const params = { method: "GET", headers: {} };
+ const params = { method: "GET", headers };
setCookieHeader(url, params);
[status, contentType, data, responseHeaders] = await httpProxy(url, params);
- if (status === 401) {
+ if (status === 401 && !widget.key) {
logger.debug("Unifi isn't logged in or rejected the reqeust, attempting login.");
if (responseHeaders?.["x-csrf-token"]) {
csrfToken = responseHeaders["x-csrf-token"];
diff --git a/src/widgets/unmanic/component.jsx b/src/widgets/unmanic/component.jsx
index 98688463..12069e52 100644
--- a/src/widgets/unmanic/component.jsx
+++ b/src/widgets/unmanic/component.jsx
@@ -1,9 +1,9 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { useEffect, useState } from "react";
-import Container from "components/services/widget/container";
-import Block from "components/services/widget/block";
-import useWidgetAPI from "utils/proxy/use-widget-api";
import { formatProxyUrl } from "utils/proxy/api-helpers";
+import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
const { widget } = service;
diff --git a/src/widgets/unmanic/widget.js b/src/widgets/unmanic/widget.js
index 4c9713e4..ef4493e9 100644
--- a/src/widgets/unmanic/widget.js
+++ b/src/widgets/unmanic/widget.js
@@ -1,5 +1,5 @@
-import genericProxyHandler from "utils/proxy/handlers/generic";
import { asJson } from "utils/proxy/api-helpers";
+import genericProxyHandler from "utils/proxy/handlers/generic";
const widget = {
api: "{url}/unmanic/api/v2/{endpoint}",
diff --git a/src/widgets/uptimekuma/component.jsx b/src/widgets/uptimekuma/component.jsx
index 01693db3..e8a42e48 100644
--- a/src/widgets/uptimekuma/component.jsx
+++ b/src/widgets/uptimekuma/component.jsx
@@ -1,8 +1,8 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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();
diff --git a/src/widgets/uptimerobot/component.jsx b/src/widgets/uptimerobot/component.jsx
index b2027a6f..a1f234c2 100644
--- a/src/widgets/uptimerobot/component.jsx
+++ b/src/widgets/uptimerobot/component.jsx
@@ -1,8 +1,8 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
import { useEffect, useState } from "react";
-import Container from "components/services/widget/container";
-import Block from "components/services/widget/block";
import { formatProxyUrl } from "utils/proxy/api-helpers";
export default function Component({ service }) {
diff --git a/src/widgets/urbackup/component.jsx b/src/widgets/urbackup/component.jsx
index c76cdf48..9d8f92ba 100644
--- a/src/widgets/urbackup/component.jsx
+++ b/src/widgets/urbackup/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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";
const Status = Object.freeze({
diff --git a/src/widgets/vikunja/component.jsx b/src/widgets/vikunja/component.jsx
index 09704338..1afccd38 100644
--- a/src/widgets/vikunja/component.jsx
+++ b/src/widgets/vikunja/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/vikunja/widget.js b/src/widgets/vikunja/widget.js
index 9a192026..8e5e680a 100644
--- a/src/widgets/vikunja/widget.js
+++ b/src/widgets/vikunja/widget.js
@@ -1,5 +1,5 @@
-import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
import { asJson } from "utils/proxy/api-helpers";
+import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = {
api: `{url}/api/v1/{endpoint}`,
diff --git a/src/widgets/watchtower/component.jsx b/src/widgets/watchtower/component.jsx
index 4bca538f..58b2a3f5 100644
--- a/src/widgets/watchtower/component.jsx
+++ b/src/widgets/watchtower/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/watchtower/proxy.js b/src/widgets/watchtower/proxy.js
index 588d08ee..484c3c7d 100644
--- a/src/widgets/watchtower/proxy.js
+++ b/src/widgets/watchtower/proxy.js
@@ -1,7 +1,7 @@
-import { httpProxy } from "utils/proxy/http";
-import { formatApiCall } from "utils/proxy/api-helpers";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
+import { formatApiCall } from "utils/proxy/api-helpers";
+import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets";
const proxyName = "watchtowerProxyHandler";
diff --git a/src/widgets/wgeasy/component.jsx b/src/widgets/wgeasy/component.jsx
index db39c81a..829b120d 100644
--- a/src/widgets/wgeasy/component.jsx
+++ b/src/widgets/wgeasy/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/whatsupdocker/component.jsx b/src/widgets/whatsupdocker/component.jsx
index 2719fa8f..cc3b5174 100644
--- a/src/widgets/whatsupdocker/component.jsx
+++ b/src/widgets/whatsupdocker/component.jsx
@@ -1,5 +1,6 @@
-import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js
index 9d4bb935..e183a9c6 100644
--- a/src/widgets/widgets.js
+++ b/src/widgets/widgets.js
@@ -1,4 +1,5 @@
import adguard from "./adguard/widget";
+import apcups from "./apcups/widget";
import argocd from "./argocd/widget";
import atsumeru from "./atsumeru/widget";
import audiobookshelf from "./audiobookshelf/widget";
@@ -24,6 +25,7 @@ import emby from "./emby/widget";
import esphome from "./esphome/widget";
import evcc from "./evcc/widget";
import fileflows from "./fileflows/widget";
+import firefly from "./firefly/widget";
import flood from "./flood/widget";
import freshrss from "./freshrss/widget";
import frigate from "./frigate/widget";
@@ -39,14 +41,15 @@ import gotify from "./gotify/widget";
import grafana from "./grafana/widget";
import hdhomerun from "./hdhomerun/widget";
import headscale from "./headscale/widget";
+import healthchecks from "./healthchecks/widget";
import homeassistant from "./homeassistant/widget";
import homebox from "./homebox/widget";
import homebridge from "./homebridge/widget";
-import healthchecks from "./healthchecks/widget";
import immich from "./immich/widget";
import jackett from "./jackett/widget";
-import jellyseerr from "./jellyseerr/widget";
import jdownloader from "./jdownloader/widget";
+import jellyseerr from "./jellyseerr/widget";
+import karakeep from "./karakeep/widget";
import kavita from "./kavita/widget";
import komga from "./komga/widget";
import kopia from "./kopia/widget";
@@ -57,9 +60,9 @@ import mailcow from "./mailcow/widget";
import mastodon from "./mastodon/widget";
import mealie from "./mealie/widget";
import medusa from "./medusa/widget";
+import mikrotik from "./mikrotik/widget";
import minecraft from "./minecraft/widget";
import miniflux from "./miniflux/widget";
-import mikrotik from "./mikrotik/widget";
import mjpeg from "./mjpeg/widget";
import moonraker from "./moonraker/widget";
import mylar from "./mylar/widget";
@@ -75,15 +78,14 @@ import octoprint from "./octoprint/widget";
import omada from "./omada/widget";
import ombi from "./ombi/widget";
import opendtu from "./opendtu/widget";
-import opnsense from "./opnsense/widget";
-import overseerr from "./overseerr/widget";
import openmediavault from "./openmediavault/widget";
import openwrt from "./openwrt/widget";
+import opnsense from "./opnsense/widget";
+import overseerr from "./overseerr/widget";
import paperlessngx from "./paperlessngx/widget";
import peanut from "./peanut/widget";
import pfsense from "./pfsense/widget";
import photoprism from "./photoprism/widget";
-import proxmoxbackupserver from "./proxmoxbackupserver/widget";
import pihole from "./pihole/widget";
import plantit from "./plantit/widget";
import plex from "./plex/widget";
@@ -92,47 +94,50 @@ import prometheus from "./prometheus/widget";
import prometheusmetric from "./prometheusmetric/widget";
import prowlarr from "./prowlarr/widget";
import proxmox from "./proxmox/widget";
+import proxmoxbackupserver from "./proxmoxbackupserver/widget";
import pterodactyl from "./pterodactyl/widget";
import pyload from "./pyload/widget";
import qbittorrent from "./qbittorrent/widget";
import qnap from "./qnap/widget";
import radarr from "./radarr/widget";
import readarr from "./readarr/widget";
+import romm from "./romm/widget";
import rutorrent from "./rutorrent/widget";
import sabnzbd from "./sabnzbd/widget";
import scrutiny from "./scrutiny/widget";
+import slskd from "./slskd/widget";
import sonarr from "./sonarr/widget";
import speedtest from "./speedtest/widget";
import spoolman from "./spoolman/widget";
import stash from "./stash/widget";
import stocks from "./stocks/widget";
import strelaysrv from "./strelaysrv/widget";
-import swagdashboard from "./swagdashboard/widget";
import suwayomi from "./suwayomi/widget";
+import swagdashboard from "./swagdashboard/widget";
import tailscale from "./tailscale/widget";
import tandoor from "./tandoor/widget";
import tautulli from "./tautulli/widget";
-import technitium from "./technitium/widget";
import tdarr from "./tdarr/widget";
+import technitium from "./technitium/widget";
import traefik from "./traefik/widget";
import transmission from "./transmission/widget";
-import tubearchivist from "./tubearchivist/widget";
import truenas from "./truenas/widget";
+import tubearchivist from "./tubearchivist/widget";
import unifi from "./unifi/widget";
import unmanic from "./unmanic/widget";
import uptimekuma from "./uptimekuma/widget";
import uptimerobot from "./uptimerobot/widget";
+import urbackup from "./urbackup/widget";
import vikunja from "./vikunja/widget";
import watchtower from "./watchtower/widget";
import wgeasy from "./wgeasy/widget";
import whatsupdocker from "./whatsupdocker/widget";
import xteve from "./xteve/widget";
-import urbackup from "./urbackup/widget";
-import romm from "./romm/widget";
import zabbix from "./zabbix/widget";
const widgets = {
adguard,
+ apcups,
argocd,
atsumeru,
audiobookshelf,
@@ -157,6 +162,7 @@ const widgets = {
esphome,
evcc,
fileflows,
+ firefly,
flood,
freshrss,
frigate,
@@ -172,6 +178,8 @@ const widgets = {
grafana,
hdhomerun,
headscale,
+ hoarder: karakeep,
+ karakeep,
homeassistant,
homebox,
homebridge,
@@ -238,6 +246,7 @@ const widgets = {
rutorrent,
sabnzbd,
scrutiny,
+ slskd,
sonarr,
speedtest,
spoolman,
diff --git a/src/widgets/xteve/component.jsx b/src/widgets/xteve/component.jsx
index 84a617c2..41f2beb0 100644
--- a/src/widgets/xteve/component.jsx
+++ b/src/widgets/xteve/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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 }) {
diff --git a/src/widgets/xteve/proxy.js b/src/widgets/xteve/proxy.js
index 453e3645..53d82bc4 100644
--- a/src/widgets/xteve/proxy.js
+++ b/src/widgets/xteve/proxy.js
@@ -1,8 +1,8 @@
+import getServiceWidget from "utils/config/service-helpers";
+import createLogger from "utils/logger";
import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
-import createLogger from "utils/logger";
import widgets from "widgets/widgets";
-import getServiceWidget from "utils/config/service-helpers";
const logger = createLogger("xteveProxyHandler");
diff --git a/src/widgets/zabbix/component.jsx b/src/widgets/zabbix/component.jsx
index ffad9f7f..b6a5b20b 100644
--- a/src/widgets/zabbix/component.jsx
+++ b/src/widgets/zabbix/component.jsx
@@ -1,7 +1,7 @@
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
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";
const PriorityUnclassified = "0";
diff --git a/tailwind.config.js b/tailwind.config.js
index 5d425938..4cc6f130 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -30,55 +30,7 @@ module.exports = {
900: "rgb(var(--color-900) / )",
},
},
- screens: {
- "3xl": "1800px",
- // => @media (min-width: 1800px) { ... }
- },
},
},
plugins: [tailwindForms, tailwindScrollbars],
- // always include these in build as classes are dynamically constructed
- safelist: [
- "backdrop-blur",
- "backdrop-blur-sm",
- "backdrop-blur-md",
- "backdrop-blur-xl",
- "backdrop-saturate-0",
- "backdrop-saturate-50",
- "backdrop-saturate-100",
- "backdrop-saturate-150",
- "backdrop-saturate-200",
- "backdrop-brightness-0",
- "backdrop-brightness-50",
- "backdrop-brightness-75",
- "backdrop-brightness-90",
- "backdrop-brightness-95",
- "backdrop-brightness-100",
- "backdrop-brightness-105",
- "backdrop-brightness-110",
- "backdrop-brightness-125",
- "backdrop-brightness-150",
- "backdrop-brightness-200",
- "grid-cols-1",
- "md:grid-cols-1",
- "md:grid-cols-2",
- "lg:grid-cols-1",
- "lg:grid-cols-2",
- "lg:grid-cols-3",
- "lg:grid-cols-4",
- "lg:grid-cols-5",
- "lg:grid-cols-6",
- "lg:grid-cols-7",
- "lg:grid-cols-8",
- // for status
- "bg-white",
- "bg-black",
- "dark:bg-white",
- "bg-orange-400",
- "dark:bg-orange-400",
- {
- pattern: /h-([0-96])/,
- variants: ["sm", "md", "lg", "xl", "2xl"],
- },
- ],
};