diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 48ae7fdc..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[Bug] " -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Configuration** -If applicable, -```yaml -# Please provide your service, widget or otherwise related configuration here -``` - -**Additional context** -Add any other context about the problem here. This includes things like: - - Service version or API version - - Docker version - - Deployment method - - Sample YAML configurations diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..fbeb7902 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,66 @@ +name: Bug report +description: Create a report to help us improve +title: "[Bug] " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Have a question? 👉 [Start a new discussion](https://github.com/benphelps/homepage/discussions/new) or [ask in chat](https://discord.gg/SaPGSzrEZC). + + Before opening an issue, please double check: + + - [The troubleshooting guide](https://gethomepage.dev/en/more/troubleshooting/). + - [The homepage documentation](https://gethomepage.dev/) + - [Existing issues](https://github.com/benphelps/homepage/search?q=&type=issues) and [discussions](https://github.com/benphelps/homepage/search?q=&type=discussions). + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what the bug is. If applicable, add screenshots to help explain your problem. + placeholder: | + Currently homepage does not work when... + + [Screenshot if applicable] + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. See error + validations: + required: true + - type: input + id: version + attributes: + label: homepage version + placeholder: e.g. v0.4.18 (4ea2798) + validations: + required: true + - type: dropdown + id: install-method + attributes: + label: Installation method + options: + - Docker + - Unraid + - Source + - Other (please describe above) + validations: + required: true + - type: textarea + id: config + attributes: + label: Configuration + description: Please provide any relevant service, widget or otherwise related configuration here + render: yaml + - type: textarea + id: other + attributes: + label: Other + description: Any other relevant details. E.g. service version or API version, docker version, etc. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 05d91b52..00000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "[Feature Request] " -labels: '' -assignees: '' - ---- - -**Is your feature request related to a service? Please describe.** -A clear and concise description of what you would like to see from this service. - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I would like it if [...] - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..5ddcb12e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,17 @@ +name: Feature request +description: Suggest an idea for this project +title: "[Feature Request] " +labels: ["enhancement"] +body: + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what you would like to see. + validations: + required: true + - type: textarea + id: other + attributes: + label: Other + description: Add any other context or information about the feature request here. diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 8f36dcc4..bacc1fa7 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -43,7 +43,7 @@ jobs: if: github.event_name != 'pull_request' uses: sigstore/cosign-installer@main with: - cosign-release: 'v1.11.0' # optional + cosign-release: 'v1.13.1' # optional # Setup QEMU # https://github.com/marketplace/actions/docker-setup-buildx#with-qemu @@ -100,7 +100,7 @@ jobs: REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} # https://github.com/docker/setup-qemu-action#about # platforms: linux/amd64,linux/arm64,linux/riscv64,linux/ppc64le,linux/s390x,linux/386,linux/mips64le,linux/mips64,linux/arm/v7,linux/arm/v6 - platforms: linux/amd64,linux/arm64,linux/arm/v7 + platforms: linux/amd64,linux/arm64,linux/arm/v7,linux/arm/v6 cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max @@ -109,13 +109,13 @@ jobs: # repository is public to avoid leaking data. If you would like to publish # transparency data even for private images, pass --force to cosign below. # https://github.com/sigstore/cosign - - name: Sign the published Docker image - if: ${{ github.event_name != 'pull_request' }} - env: - COSIGN_EXPERIMENTAL: "true" - # This step uses the identity token to provision an ephemeral certificate - # against the sigstore community Fulcio instance. - run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} +# - name: Sign the published Docker image +# if: ${{ github.event_name != 'pull_request' }} +# env: +# COSIGN_EXPERIMENTAL: "true" +# # This step uses the identity token to provision an ephemeral certificate +# # against the sigstore community Fulcio instance. +# run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} # Temp fix # https://github.com/docker/build-push-action/issues/252 diff --git a/Dockerfile b/Dockerfile index 8921388b..48e5d2f3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # syntax = docker/dockerfile:latest # Install dependencies only when needed -FROM node:current-alpine AS deps +FROM docker.io/node:18-alpine AS deps WORKDIR /app @@ -19,7 +19,7 @@ RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm f RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store pnpm install -r --offline # Rebuild the source code only when needed -FROM node:current-alpine AS builder +FROM docker.io/node:18-alpine AS builder WORKDIR /app ARG BUILDTIME @@ -37,7 +37,7 @@ RUN < - - {dateFormat.format(date)} - +
+
+ + {dateFormat.format(date)} + +
); } diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index e87bd90a..e99303ab 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -1,6 +1,7 @@ /* 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"; @@ -18,6 +19,10 @@ function MyApp({ Component, pageProps }) { fetcher: (resource, init) => fetch(resource, init).then((res) => res.json()), }} > + + {/* https://nextjs.org/docs/messages/no-document-viewport-meta */} + + diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx index 5f9e06fb..7e02a005 100644 --- a/src/pages/_document.jsx +++ b/src/pages/_document.jsx @@ -9,7 +9,6 @@ export default function Document() { content="A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations." /> - diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 1e2714c8..88c25cc4 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -35,7 +35,7 @@ const Version = dynamic(() => import("components/version"), { ssr: false, }); -const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "search", "datetime"]; +const rightAlignedWidgets = ["weatherapi", "openweathermap", "weather", "openmeteo", "search", "datetime"]; export async function getStaticProps() { let logger; @@ -272,7 +272,7 @@ function Home({ initialSettings }) { )} {bookmarks && ( -
+
{bookmarks.map((group) => ( ))} diff --git a/src/skeleton/bookmarks.yaml b/src/skeleton/bookmarks.yaml index 871e9134..5a33593a 100644 --- a/src/skeleton/bookmarks.yaml +++ b/src/skeleton/bookmarks.yaml @@ -1,6 +1,6 @@ --- # For configuration options and examples, please see: -# https://github.com/benphelps/homepage/wiki/Bookmarks +# https://gethomepage.dev/en/configs/bookmarks - Developer: - Github: diff --git a/src/skeleton/docker.yaml b/src/skeleton/docker.yaml index 724c2281..27850e83 100644 --- a/src/skeleton/docker.yaml +++ b/src/skeleton/docker.yaml @@ -1,6 +1,6 @@ --- # For configuration options and examples, please see: -# https://github.com/benphelps/homepage/wiki/Docker-Integration +# https://gethomepage.dev/en/configs/docker/ # my-docker: # host: 127.0.0.1 diff --git a/src/skeleton/services.yaml b/src/skeleton/services.yaml index 9514f67e..32378898 100644 --- a/src/skeleton/services.yaml +++ b/src/skeleton/services.yaml @@ -1,6 +1,6 @@ --- # For configuration options and examples, please see: -# https://github.com/benphelps/homepage/wiki/Services +# https://gethomepage.dev/en/configs/services - My First Group: - My First Service: diff --git a/src/skeleton/settings.yaml b/src/skeleton/settings.yaml index 623781c6..2d0d0de0 100644 --- a/src/skeleton/settings.yaml +++ b/src/skeleton/settings.yaml @@ -1,6 +1,6 @@ --- # For configuration options and examples, please see: -# https://github.com/benphelps/homepage/wiki/Settings +# https://gethomepage.dev/en/configs/settings providers: openweathermap: openweathermapapikey diff --git a/src/skeleton/widgets.yaml b/src/skeleton/widgets.yaml index a5c6a911..c21f674b 100644 --- a/src/skeleton/widgets.yaml +++ b/src/skeleton/widgets.yaml @@ -1,6 +1,6 @@ --- # For configuration options and examples, please see: -# https://github.com/benphelps/homepage/wiki/Information-Widgets +# https://gethomepage.dev/en/configs/widgets - resources: cpu: true diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index 7418b68c..54c393b1 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -31,6 +31,10 @@ export default async function credentialedProxyHandler(req, res) { headers.Authorization = `Bearer ${widget.key}`; } else if (widget.type === "proxmox") { headers.Authorization = `PVEAPIToken=${widget.username}=${widget.password}`; + } else if (widget.type === "autobrr") { + headers["X-API-Token"] = `${widget.key}`; + } else if (widget.type === "tubearchivist") { + headers.Authorization = `Token ${widget.key}`; } else { headers["X-API-Key"] = `${widget.key}`; } diff --git a/src/widgets/autobrr/component.jsx b/src/widgets/autobrr/component.jsx new file mode 100644 index 00000000..b78f48f6 --- /dev/null +++ b/src/widgets/autobrr/component.jsx @@ -0,0 +1,39 @@ +import { useTranslation } from "next-i18next"; + +import Container from "components/services/widget/container"; +import Block from "components/services/widget/block"; +import useWidgetAPI from "utils/proxy/use-widget-api"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + + const { data: statsData, error: statsError } = useWidgetAPI(widget, "stats"); + const { data: filtersData, error: filtersError } = useWidgetAPI(widget, "filters"); + const { data: indexersData, error: indexersError } = useWidgetAPI(widget, "indexers"); + + if (statsError || filtersError || indexersError) { + return ; + } + + if (!statsData || !filtersData || !indexersData) { + return ( + + + + + + + ); + } + + return ( + + + + + + + ); +} diff --git a/src/widgets/autobrr/widget.js b/src/widgets/autobrr/widget.js new file mode 100644 index 00000000..0254029e --- /dev/null +++ b/src/widgets/autobrr/widget.js @@ -0,0 +1,20 @@ +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: credentialedProxyHandler, + + mappings: { + stats: { + endpoint: "release/stats", + }, + filters: { + endpoint: "filters", + }, + indexers: { + endpoint: "release/indexers", + }, + }, +}; + +export default widget; diff --git a/src/widgets/components.js b/src/widgets/components.js index da6ce362..47502403 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -3,12 +3,14 @@ import dynamic from "next/dynamic"; const components = { adguard: dynamic(() => import("./adguard/component")), authentik: dynamic(() => import("./authentik/component")), + autobrr: dynamic(() => import("./autobrr/component")), bazarr: dynamic(() => import("./bazarr/component")), changedetectionio: dynamic(() => import("./changedetectionio/component")), coinmarketcap: dynamic(() => import("./coinmarketcap/component")), docker: dynamic(() => import("./docker/component")), emby: dynamic(() => import("./emby/component")), gotify: dynamic(() => import("./gotify/component")), + homebridge: dynamic(() => import("./homebridge/component")), jackett: dynamic(() => import("./jackett/component")), jellyfin: dynamic(() => import("./emby/component")), jellyseerr: dynamic(() => import("./jellyseerr/component")), @@ -34,7 +36,9 @@ const components = { tautulli: dynamic(() => import("./tautulli/component")), traefik: dynamic(() => import("./traefik/component")), transmission: dynamic(() => import("./transmission/component")), + tubearchivist: dynamic(() => import("./tubearchivist/component")), unifi: dynamic(() => import("./unifi/component")), + watchtower: dynamic(() => import("./watchtower/component")), }; export default components; diff --git a/src/widgets/emby/widget.js b/src/widgets/emby/widget.js index 1bb5a726..27fc749b 100644 --- a/src/widgets/emby/widget.js +++ b/src/widgets/emby/widget.js @@ -10,7 +10,7 @@ const widget = { }, PlayControl: { method: "POST", - enpoint: "Sessions/{sessionId}/Playing/{command}", + endpoint: "Sessions/{sessionId}/Playing/{command}", segments: ["sessionId", "command"], }, }, diff --git a/src/widgets/homebridge/component.jsx b/src/widgets/homebridge/component.jsx new file mode 100644 index 00000000..807cc49a --- /dev/null +++ b/src/widgets/homebridge/component.jsx @@ -0,0 +1,51 @@ +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: homebridgeData, error: homebridgeError } = useWidgetAPI(widget, "info"); + + if (homebridgeError || homebridgeData?.error) { + return ; + } + + if (!homebridgeData) { + return ( + + + + + + ); + } + + return ( + + + + {homebridgeData?.childBridges?.total > 0 && + } + + ); +} diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js new file mode 100644 index 00000000..3f81051f --- /dev/null +++ b/src/widgets/homebridge/proxy.js @@ -0,0 +1,106 @@ +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 widgets from "widgets/widgets"; + +const proxyName = "homebridgeProxyHandler"; +const sessionTokenCacheKey = `${proxyName}__sessionToken`; +const logger = createLogger(proxyName); + +async function login(widget) { + const endpoint = "auth/login"; + const api = widgets?.[widget.type]?.api + const loginUrl = new URL(formatApiCall(api, { endpoint, ...widget })); + const loginBody = { username: widget.username, password: widget.password }; + const headers = { "Content-Type": "application/json" }; + // eslint-disable-next-line no-unused-vars + const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, { + method: "POST", + body: JSON.stringify(loginBody), + headers, + }); + + try { + const { access_token: accessToken, expires_in: expiresIn } = JSON.parse(data.toString()); + + cache.put(sessionTokenCacheKey, accessToken, (expiresIn * 1000) - 5 * 60 * 1000); // expiresIn (s) - 5m + return { accessToken }; + } catch (e) { + logger.error("Unable to login to Homebridge API: %s", e); + } + + return { accessToken: false }; +} + +async function apiCall(widget, endpoint) { + const headers = { + "content-type": "application/json", + "Authorization": `Bearer ${cache.get(sessionTokenCacheKey)}`, + } + + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + const method = "GET"; + + let [status, contentType, data, responseHeaders] = await httpProxy(url, { + method, + headers, + }); + + if (status === 401) { + logger.debug("Homebridge API rejected the request, attempting to obtain new session token"); + const { accessToken } = login(widget); + headers.Authorization = `Bearer ${accessToken}`; + + // retry the request, now with the new session token + [status, contentType, data, responseHeaders] = await httpProxy(url, { + method, + headers, + }); + } + + if (status !== 200) { + logger.error("Error getting data from Homebridge: %d. Data: %s", status, data); + } + + return { status, contentType, data: JSON.parse(data.toString()), responseHeaders }; +} + +export default async function homebridgeProxyHandler(req, res) { + const { group, service } = 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); + + 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" }); + } + + if (!cache.get(sessionTokenCacheKey)) { + await login(widget); + } + + const { data: statusData } = await apiCall(widget, "status/homebridge"); + const { data: versionData } = await apiCall(widget, "status/homebridge-version"); + const { data: childBridgeData } = await apiCall(widget, "status/homebridge/child-bridges"); + const { data: pluginsData } = await apiCall(widget, "plugins"); + + return res.status(200).send({ + status: statusData?.status, + updateAvailable: versionData?.updateAvailable, + plugins: { + updatesAvailable: pluginsData?.filter(p => p.updateAvailable).length, + }, + childBridges: { + running: childBridgeData?.filter(cb => cb.status === "ok").length, + total: childBridgeData?.length + } + }); +} diff --git a/src/widgets/homebridge/widget.js b/src/widgets/homebridge/widget.js new file mode 100644 index 00000000..31d4f30e --- /dev/null +++ b/src/widgets/homebridge/widget.js @@ -0,0 +1,14 @@ +import homebridgeProxyHandler from "./proxy"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: homebridgeProxyHandler, + + mappings: { + info: { + endpoint: "/", + } + }, +}; + +export default widget; diff --git a/src/widgets/tubearchivist/component.jsx b/src/widgets/tubearchivist/component.jsx new file mode 100644 index 00000000..5b548443 --- /dev/null +++ b/src/widgets/tubearchivist/component.jsx @@ -0,0 +1,40 @@ +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: downloadsData, error: downloadsError } = useWidgetAPI(widget, "downloads"); + const { data: videosData, error: videosError } = useWidgetAPI(widget, "videos"); + const { data: channelsData, error: channelsError } = useWidgetAPI(widget, "channels"); + const { data: playlistsData, error: playlistsError } = useWidgetAPI(widget, "playlists"); + + if (downloadsError || videosError || channelsError || playlistsError) { + return ; + } + + if (!downloadsData || !videosData || !channelsData || !playlistsData) { + return ( + + + + + + + ); + } + + return ( + + + + + + + ); +} diff --git a/src/widgets/tubearchivist/widget.js b/src/widgets/tubearchivist/widget.js new file mode 100644 index 00000000..c73070f0 --- /dev/null +++ b/src/widgets/tubearchivist/widget.js @@ -0,0 +1,23 @@ +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: credentialedProxyHandler, + + mappings: { + downloads: { + endpoint: "download", + }, + videos: { + endpoint: "video", + }, + channels: { + endpoint: "channel", + }, + playlists: { + endpoint: "playlist", + }, + }, +}; + +export default widget; diff --git a/src/widgets/watchtower/component.jsx b/src/widgets/watchtower/component.jsx new file mode 100644 index 00000000..68c5531f --- /dev/null +++ b/src/widgets/watchtower/component.jsx @@ -0,0 +1,36 @@ +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: watchData, error: watchError } = useWidgetAPI(widget, "watchtower"); + + if (watchError || !watchData) { + return ; + } + + if (!watchData) { + return ( + + + + + + ); + } + + return ( + + + + + + ); +} diff --git a/src/widgets/watchtower/proxy.js b/src/widgets/watchtower/proxy.js new file mode 100644 index 00000000..2d54928c --- /dev/null +++ b/src/widgets/watchtower/proxy.js @@ -0,0 +1,48 @@ +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 widgets from "widgets/widgets"; + +const proxyName = "watchtowerProxyHandler"; +const logger = createLogger(proxyName); + +export default async function watchtowerProxyHandler(req, res) { + const { group, service, endpoint } = 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); + + 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(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + + const [status, contentType, data] = await httpProxy(url, { + method: "GET", + headers: { + "Authorization": `Bearer ${widget.key}`, + } + }); + + if (status !== 200 || !data) { + logger.error("Error getting data from WatchTower: %d. Data: %s", status, data); + } + + const cleanData = data.toString().split("\n").filter(s => s.startsWith("watchtower")) + const jsonRes = {} + + cleanData.map(e => e.split(" ")).forEach(strArray => { + const [key, value] = strArray + jsonRes[key] = value + }) + + if (contentType) res.setHeader("Content-Type", contentType); + return res.status(status).send(jsonRes); +} diff --git a/src/widgets/watchtower/widget.js b/src/widgets/watchtower/widget.js new file mode 100644 index 00000000..6e8fdf66 --- /dev/null +++ b/src/widgets/watchtower/widget.js @@ -0,0 +1,14 @@ +import watchtowerProxyHandler from "./proxy"; + +const widget = { + api: "{url}/{endpoint}", + proxyHandler: watchtowerProxyHandler, + + mappings: { + "watchtower": { + endpoint: "v1/metrics", + }, + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index a90bd620..0352466d 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -1,10 +1,12 @@ import adguard from "./adguard/widget"; import authentik from "./authentik/widget"; +import autobrr from "./autobrr/widget"; import bazarr from "./bazarr/widget"; import changedetectionio from "./changedetectionio/widget"; import coinmarketcap from "./coinmarketcap/widget"; import emby from "./emby/widget"; import gotify from "./gotify/widget"; +import homebridge from "./homebridge/widget"; import jackett from "./jackett/widget"; import jellyseerr from "./jellyseerr/widget"; import lidarr from "./lidarr/widget"; @@ -29,16 +31,20 @@ import strelaysrv from "./strelaysrv/widget"; import tautulli from "./tautulli/widget"; import traefik from "./traefik/widget"; import transmission from "./transmission/widget"; +import tubearchivist from "./tubearchivist/widget"; import unifi from "./unifi/widget"; +import watchtower from './watchtower/widget' const widgets = { adguard, authentik, + autobrr, bazarr, changedetectionio, coinmarketcap, emby, gotify, + homebridge, jackett, jellyfin: emby, jellyseerr, @@ -64,8 +70,10 @@ const widgets = { tautulli, traefik, transmission, + tubearchivist, unifi, unifi_console: unifi, + watchtower, }; export default widgets;