From 897309a47cb94ff9d2d45eb875e7a09ff291b5dc Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 24 Nov 2024 22:43:13 -0800 Subject: [PATCH 01/66] Enhancement: resources network widget (#4327) --- docs/installation/k8s.md | 3 +- docs/widgets/info/resources.md | 3 +- src/components/widgets/resources/network.jsx | 47 +++++++++++++++++++ .../widgets/resources/resources.jsx | 2 + src/components/widgets/widget/resource.jsx | 7 ++- src/pages/api/widgets/resources.js | 28 ++++++++++- 6 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 src/components/widgets/resources/network.jsx diff --git a/docs/installation/k8s.md b/docs/installation/k8s.md index 6805139b..24be2c34 100644 --- a/docs/installation/k8s.md +++ b/docs/installation/k8s.md @@ -175,6 +175,7 @@ data: expanded: true cpu: true memory: true + network: default - search: provider: duckduckgo target: _blank @@ -370,7 +371,7 @@ prevent unnecessary re-renders on page loads and window / tab focusing. The procedure for enabling sticky sessions depends on your Ingress controller. Below is an example using Traefik as the Ingress controller. -``` +```yaml apiVersion: traefik.io/v1alpha1 kind: IngressRoute metadata: diff --git a/docs/widgets/info/resources.md b/docs/widgets/info/resources.md index 19323dc3..7fcf9c5c 100644 --- a/docs/widgets/info/resources.md +++ b/docs/widgets/info/resources.md @@ -24,9 +24,10 @@ _Note: unfortunately, the package used for getting CPU temp ([systeminformation] tempmin: 0 # optional, minimum cpu temp tempmax: 100 # optional, maximum cpu temp uptime: true - units: imperial # only used by cpu temp + units: imperial # only used by cpu temp, options: 'imperial' or 'metric' refresh: 3000 # optional, in ms diskUnits: bytes # optional, bytes (default) or bbytes. Only applies to disk + network: true # optional, uses 'default' if true or specify a network interface name ``` You can also pass a `label` option, which allows you to group resources under named sections, diff --git a/src/components/widgets/resources/network.jsx b/src/components/widgets/resources/network.jsx new file mode 100644 index 00000000..5b5cc004 --- /dev/null +++ b/src/components/widgets/resources/network.jsx @@ -0,0 +1,47 @@ +import useSWR from "swr"; +import { FaNetworkWired } from "react-icons/fa"; +import { useTranslation } from "next-i18next"; + +import Resource from "../widget/resource"; +import Error from "../widget/error"; + +export default function Network({ options, refresh = 1500 }) { + const { t } = useTranslation(); + // eslint-disable-next-line no-param-reassign + if (options.network === true) options.network = "default"; + + const { data, error } = useSWR(`/api/widgets/resources?type=network&interfaceName=${options.network}`, { + refreshInterval: refresh, + }); + + if (error || data?.error) { + return ; + } + + if (!data || !data.network) { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/components/widgets/resources/resources.jsx b/src/components/widgets/resources/resources.jsx index 634e0ff5..db26caa7 100644 --- a/src/components/widgets/resources/resources.jsx +++ b/src/components/widgets/resources/resources.jsx @@ -6,6 +6,7 @@ import Cpu from "./cpu"; import Memory from "./memory"; import CpuTemp from "./cputemp"; import Uptime from "./uptime"; +import Network from "./network"; export default function Resources({ options }) { const { expanded, units, diskUnits, tempmin, tempmax } = options; @@ -23,6 +24,7 @@ export default function Resources({ options }) { )) : options.disk && } + {options.network && } {options.cputemp && ( )} diff --git a/src/components/widgets/widget/resource.jsx b/src/components/widgets/widget/resource.jsx index 8c975928..b1f73740 100644 --- a/src/components/widgets/widget/resource.jsx +++ b/src/components/widgets/widget/resource.jsx @@ -10,6 +10,7 @@ export default function Resource({ percentage, expanded = false, additionalClassNames = "", + wide = false, }) { const Icon = icon; @@ -18,7 +19,11 @@ export default function Resource({ className={`flex-none flex flex-row items-center mr-3 py-1.5 information-widget-resource ${additionalClassNames}`} > -
+
{value}
{label}
diff --git a/src/pages/api/widgets/resources.js b/src/pages/api/widgets/resources.js index 66449bff..4df544e8 100644 --- a/src/pages/api/widgets/resources.js +++ b/src/pages/api/widgets/resources.js @@ -7,7 +7,7 @@ const logger = createLogger("resources"); const si = require("systeminformation"); export default async function handler(req, res) { - const { type, target } = req.query; + const { type, target, interfaceName = "default" } = req.query; if (type === "cpu") { const load = await si.currentLoad(); @@ -57,6 +57,32 @@ export default async function handler(req, res) { }); } + if (type === "network") { + let networkData = await si.networkStats(); + let interfaceDefault; + logger.debug("networkData:", JSON.stringify(networkData)); + if (interfaceName && interfaceName !== "default") { + networkData = networkData.filter((network) => network.iface === interfaceName).at(0); + if (!networkData) { + return res.status(404).json({ + error: "Interface not found", + }); + } + } else { + interfaceDefault = await si.networkInterfaceDefault(); + networkData = networkData.filter((network) => network.iface === interfaceDefault).at(0); + if (!networkData) { + return res.status(404).json({ + error: "Default interface not found", + }); + } + } + return res.status(200).json({ + network: networkData, + interface: interfaceName !== "default" ? interfaceName : interfaceDefault, + }); + } + return res.status(400).json({ error: "invalid type", }); From cbf304a4c81df5bbec1164bae858a3d684867bc8 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 24 Nov 2024 22:43:17 -0800 Subject: [PATCH 02/66] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f898387..58820942 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Homepage has built-in support for Docker, and can automatically discover and add ## Service Widgets -Homepage also has support for over 100 3rd party services, including all popular starr apps, and most popular self-hosted apps. Some examples include: Radarr, Sonarr, Lidarr, Bazarr, Ombi, Tautulli, Plex, Jellyfin, Emby, Transmission, qBittorrent, Deluge, Jackett, NZBGet, SABnzbd, etc. As well as service integrations, Homepage also has a number of information providers, sourcing information from a variety of external 3rd party APIs. See the [Service](https://gethomepage.dev/widgets/) page for more information. +Homepage also has support for hundreds of 3rd-party services, including all popular \*arr apps, and most popular self-hosted apps. Some examples include: Radarr, Sonarr, Lidarr, Bazarr, Ombi, Tautulli, Plex, Jellyfin, Emby, Transmission, qBittorrent, Deluge, Jackett, NZBGet, SABnzbd, etc. As well as service integrations, Homepage also has a number of information providers, sourcing information from a variety of external 3rd-party APIs. See the [Service](https://gethomepage.dev/widgets/) page for more information. ## Information Widgets From 385511f773daea10bd27c5673231582131f00b86 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 24 Nov 2024 23:11:07 -0800 Subject: [PATCH 03/66] Fix: resources network better startup behavior --- src/components/widgets/resources/network.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/widgets/resources/network.jsx b/src/components/widgets/resources/network.jsx index 5b5cc004..a2a3acac 100644 --- a/src/components/widgets/resources/network.jsx +++ b/src/components/widgets/resources/network.jsx @@ -18,7 +18,7 @@ export default function Network({ options, refresh = 1500 }) { return ; } - if (!data || !data.network) { + if (!data || !data.network || !data.network.rx_sec || !data.network.tx_sec) { return ( Date: Wed, 27 Nov 2024 02:33:40 -0800 Subject: [PATCH 04/66] Enhancement: multiple widgets per service (#4338) --- docs/configs/docker.md | 12 ++ docs/configs/service-widgets.md | 20 ++- docs/widgets/index.md | 11 +- src/components/services/item.jsx | 4 +- src/components/services/widget.jsx | 10 +- src/pages/api/services/proxy.js | 6 +- src/utils/config/service-helpers.js | 159 ++++++++++++----------- src/utils/proxy/api-helpers.js | 1 + src/utils/proxy/handlers/credentialed.js | 4 +- src/utils/proxy/handlers/generic.js | 4 +- src/utils/proxy/handlers/jsonrpc.js | 4 +- src/utils/proxy/handlers/synology.js | 4 +- src/widgets/audiobookshelf/proxy.js | 4 +- src/widgets/beszel/proxy.js | 4 +- src/widgets/calendar/proxy.js | 4 +- src/widgets/crowdsec/proxy.js | 4 +- src/widgets/deluge/proxy.js | 4 +- src/widgets/flood/proxy.js | 4 +- src/widgets/freshrss/proxy.js | 4 +- src/widgets/fritzbox/proxy.js | 4 +- src/widgets/gamedig/proxy.js | 4 +- src/widgets/homeassistant/proxy.js | 4 +- src/widgets/homebox/proxy.js | 4 +- src/widgets/homebridge/proxy.js | 4 +- src/widgets/jackett/proxy.js | 4 +- src/widgets/jdownloader/proxy.js | 4 +- src/widgets/kavita/proxy.js | 4 +- src/widgets/minecraft/proxy.js | 4 +- src/widgets/npm/proxy.js | 4 +- src/widgets/omada/proxy.js | 4 +- src/widgets/openmediavault/proxy.js | 4 +- src/widgets/openwrt/proxy.js | 4 +- src/widgets/photoprism/proxy.js | 4 +- src/widgets/pihole/proxy.js | 4 +- src/widgets/plex/proxy.js | 4 +- src/widgets/pyload/proxy.js | 4 +- src/widgets/qbittorrent/proxy.js | 4 +- src/widgets/qnap/proxy.js | 4 +- src/widgets/rutorrent/proxy.js | 4 +- src/widgets/suwayomi/proxy.js | 4 +- src/widgets/tdarr/proxy.js | 4 +- src/widgets/transmission/proxy.js | 4 +- src/widgets/unifi/proxy.js | 8 +- src/widgets/urbackup/proxy.js | 4 +- src/widgets/watchtower/proxy.js | 4 +- src/widgets/xteve/proxy.js | 4 +- 46 files changed, 210 insertions(+), 169 deletions(-) diff --git a/docs/configs/docker.md b/docs/configs/docker.md index 51f6b523..7cea1fdc 100644 --- a/docs/configs/docker.md +++ b/docs/configs/docker.md @@ -153,6 +153,18 @@ labels: - homepage.widget.fields=["field1","field2"] # optional ``` +Multiple widgets can be specified by incrementing the index, e.g. + +```yaml +labels: ... + - homepage.widget[0].type=emby + - homepage.widget[0].url=http://emby.home + - homepage.widget[0].key=yourembyapikeyhere + - homepage.widget[1].type=uptimekuma + - homepage.widget[1].url=http://uptimekuma.home + - homepage.widget[1].slug=youreventslughere +``` + You can add specify fields for e.g. the [CustomAPI](../widgets/services/customapi.md) widget by using array-style dot notation: ```yaml diff --git a/docs/configs/service-widgets.md b/docs/configs/service-widgets.md index 9c54964e..df696f61 100644 --- a/docs/configs/service-widgets.md +++ b/docs/configs/service-widgets.md @@ -5,7 +5,7 @@ description: Service Widget Configuration Unless otherwise noted, URLs should not end with a `/` or other API path. Each widget will handle the path on its own. -Each service can have one widget attached to it (often matching the service type, but that's not forced). +Each service can have widgets attached to it (often matching the service type, but that's not forced). In addition to the href of the service, you can also specify the target location in which to open that link. See [Link Target](settings.md#link-target) for more details. @@ -22,6 +22,24 @@ Using Emby as an example, this is how you would attach the Emby service widget. key: apikeyapikeyapikeyapikeyapikey ``` +## Multiple Widgets + +Each service can have multiple widgets attached to it, for example: + +```yaml +- Emby: + icon: emby.png + href: http://emby.host.or.ip/ + description: Movies & TV Shows + widgets: + - type: emby + url: http://emby.host.or.ip + key: apikeyapikeyapikeyapikeyapikey + - type: uptimekuma + url: http://uptimekuma.host.or.ip:port + slug: statuspageslug +``` + ## Field Visibility Each widget can optionally provide a list of which fields should be visible via the `fields` widget property. If no fields are specified, then all fields will be displayed. The `fields` property must be a valid YAML array of strings. As an example, here is the entry for Sonarr showing only a couple of fields. diff --git a/docs/widgets/index.md b/docs/widgets/index.md index 8b81ee40..4bd45af7 100644 --- a/docs/widgets/index.md +++ b/docs/widgets/index.md @@ -19,10 +19,13 @@ Service widgets are used to display the status of a service, often a web service description: Watch movies and TV shows. server: localhost container: plex - widget: - type: tautulli - url: http://172.16.1.1:8181 - key: aabbccddeeffgghhiijjkkllmmnnoo + widgets: + - type: tautulli + url: http://172.16.1.1:8181 + key: aabbccddeeffgghhiijjkkllmmnnoo + - type: uptimekuma + url: http://172.16.1.2:8080 + slug: aaaaaaabbbbb ``` ## Info Widgets diff --git a/src/components/services/item.jsx b/src/components/services/item.jsx index a38dfaa3..54560d6f 100644 --- a/src/components/services/item.jsx +++ b/src/components/services/item.jsx @@ -154,7 +154,9 @@ export default function Item({ service, group, useEqualHeights }) {
)} - {service.widget && } + {service.widgets.map((widget) => ( + + ))}
); diff --git a/src/components/services/widget.jsx b/src/components/services/widget.jsx index 292b2b1c..61a21a66 100644 --- a/src/components/services/widget.jsx +++ b/src/components/services/widget.jsx @@ -3,22 +3,24 @@ import { useTranslation } from "next-i18next"; import ErrorBoundary from "components/errorboundry"; import components from "widgets/components"; -export default function Widget({ service }) { +export default function Widget({ widget, service }) { const { t } = useTranslation("common"); - const ServiceWidget = components[service.widget.type]; + const ServiceWidget = components[widget.type]; + const fullService = Object.apply({}, service); + fullService.widget = widget; if (ServiceWidget) { return ( - + ); } return (
-
{t("widget.missing_type", { type: service.widget.type })}
+
{t("widget.missing_type", { type: widget.type })}
); } diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js index 90280c3d..3f8adc88 100644 --- a/src/pages/api/services/proxy.js +++ b/src/pages/api/services/proxy.js @@ -9,8 +9,8 @@ const logger = createLogger("servicesProxy"); export default async function handler(req, res) { try { - const { service, group } = req.query; - const serviceWidget = await getServiceWidget(group, service); + const { service, group, index } = req.query; + const serviceWidget = await getServiceWidget(group, service, index); let type = serviceWidget?.type; // exceptions @@ -41,7 +41,7 @@ export default async function handler(req, res) { const endpoint = mapping?.endpoint; const endpointProxy = mapping?.proxyHandler || serviceProxyHandler; - if (mapping.method && mapping.method !== req.method) { + if (mapping?.method && mapping.method !== req.method) { logger.debug("Unsupported method: %s", req.method); return res.status(403).json({ error: "Unsupported method" }); } diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index ea82c735..e6ef6173 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -354,8 +354,12 @@ export function cleanServiceGroups(groups) { if (typeof cleanedService.weight !== "number") { cleanedService.weight = 0; } - + if (!cleanedService.widgets) cleanedService.widgets = []; if (cleanedService.widget) { + cleanedService.widgets.push(cleanedService.widget); + delete cleanedService.widget; + } + cleanedService.widgets = cleanedService.widgets.map((widgetData, index) => { // whitelisted set of keys to pass to the frontend // alphabetical, grouped by widget(s) const { @@ -495,7 +499,7 @@ export function cleanServiceGroups(groups) { // spoolman spoolIds, - } = cleanedService.widget; + } = widgetData; let fieldsList = fields; if (typeof fields === "string") { @@ -507,160 +511,160 @@ export function cleanServiceGroups(groups) { } } - cleanedService.widget = { + const widget = { type, fields: fieldsList || null, hide_errors: hideErrors || false, service_name: service.name, service_group: serviceGroup.name, + index, }; if (type === "azuredevops") { - if (userEmail) cleanedService.widget.userEmail = userEmail; - if (repositoryId) cleanedService.widget.repositoryId = repositoryId; + if (userEmail) widget.userEmail = userEmail; + if (repositoryId) widget.repositoryId = repositoryId; } if (type === "beszel") { - if (systemId) cleanedService.widget.systemId = systemId; + if (systemId) widget.systemId = systemId; } if (type === "coinmarketcap") { - if (currency) cleanedService.widget.currency = currency; - if (symbols) cleanedService.widget.symbols = symbols; - if (slugs) cleanedService.widget.slugs = slugs; - if (defaultinterval) cleanedService.widget.defaultinterval = defaultinterval; + if (currency) widget.currency = currency; + if (symbols) widget.symbols = symbols; + if (slugs) widget.slugs = slugs; + if (defaultinterval) widget.defaultinterval = defaultinterval; } if (type === "docker") { - if (server) cleanedService.widget.server = server; - if (container) cleanedService.widget.container = container; + if (server) widget.server = server; + if (container) widget.container = container; } if (type === "unifi") { - if (site) cleanedService.widget.site = site; + if (site) widget.site = site; } if (type === "proxmox") { - if (node) cleanedService.widget.node = node; + if (node) widget.node = node; } if (type === "kubernetes") { - if (namespace) cleanedService.widget.namespace = namespace; - if (app) cleanedService.widget.app = app; - if (podSelector) cleanedService.widget.podSelector = podSelector; + if (namespace) widget.namespace = namespace; + if (app) widget.app = app; + if (podSelector) widget.podSelector = podSelector; } if (type === "iframe") { - if (src) cleanedService.widget.src = src; - if (classes) cleanedService.widget.classes = classes; - if (referrerPolicy) cleanedService.widget.referrerPolicy = referrerPolicy; - if (allowPolicy) cleanedService.widget.allowPolicy = allowPolicy; - if (allowFullscreen) cleanedService.widget.allowFullscreen = allowFullscreen; - if (loadingStrategy) cleanedService.widget.loadingStrategy = loadingStrategy; - if (allowScrolling) cleanedService.widget.allowScrolling = allowScrolling; - if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval; + if (src) widget.src = src; + if (classes) widget.classes = classes; + if (referrerPolicy) widget.referrerPolicy = referrerPolicy; + if (allowPolicy) widget.allowPolicy = allowPolicy; + if (allowFullscreen) widget.allowFullscreen = allowFullscreen; + if (loadingStrategy) widget.loadingStrategy = loadingStrategy; + if (allowScrolling) widget.allowScrolling = allowScrolling; + if (refreshInterval) widget.refreshInterval = refreshInterval; } if (["opnsense", "pfsense"].includes(type)) { - if (wan) cleanedService.widget.wan = wan; + if (wan) widget.wan = wan; } if (["emby", "jellyfin"].includes(type)) { - if (enableBlocks !== undefined) cleanedService.widget.enableBlocks = JSON.parse(enableBlocks); - if (enableNowPlaying !== undefined) cleanedService.widget.enableNowPlaying = JSON.parse(enableNowPlaying); + if (enableBlocks !== undefined) widget.enableBlocks = JSON.parse(enableBlocks); + if (enableNowPlaying !== undefined) widget.enableNowPlaying = JSON.parse(enableNowPlaying); } if (["emby", "jellyfin", "tautulli"].includes(type)) { if (expandOneStreamToTwoRows !== undefined) - cleanedService.widget.expandOneStreamToTwoRows = !!JSON.parse(expandOneStreamToTwoRows); - if (showEpisodeNumber !== undefined) - cleanedService.widget.showEpisodeNumber = !!JSON.parse(showEpisodeNumber); - if (enableUser !== undefined) cleanedService.widget.enableUser = !!JSON.parse(enableUser); + widget.expandOneStreamToTwoRows = !!JSON.parse(expandOneStreamToTwoRows); + if (showEpisodeNumber !== undefined) widget.showEpisodeNumber = !!JSON.parse(showEpisodeNumber); + if (enableUser !== undefined) widget.enableUser = !!JSON.parse(enableUser); } if (["sonarr", "radarr"].includes(type)) { - if (enableQueue !== undefined) cleanedService.widget.enableQueue = JSON.parse(enableQueue); + if (enableQueue !== undefined) widget.enableQueue = JSON.parse(enableQueue); } if (type === "truenas") { - if (enablePools !== undefined) cleanedService.widget.enablePools = JSON.parse(enablePools); - if (nasType !== undefined) cleanedService.widget.nasType = nasType; + if (enablePools !== undefined) widget.enablePools = JSON.parse(enablePools); + if (nasType !== undefined) widget.nasType = nasType; } if (["diskstation", "qnap"].includes(type)) { - if (volume) cleanedService.widget.volume = volume; + if (volume) widget.volume = volume; } if (type === "kopia") { - if (snapshotHost) cleanedService.widget.snapshotHost = snapshotHost; - if (snapshotPath) cleanedService.widget.snapshotPath = snapshotPath; + if (snapshotHost) widget.snapshotHost = snapshotHost; + if (snapshotPath) widget.snapshotPath = snapshotPath; } if (["glances", "immich", "mealie", "pfsense", "pihole"].includes(type)) { - if (version) cleanedService.widget.version = parseInt(version, 10); + if (version) widget.version = parseInt(version, 10); } if (type === "glances") { - if (metric) cleanedService.widget.metric = metric; + if (metric) widget.metric = metric; if (chart !== undefined) { - cleanedService.widget.chart = chart; + widget.chart = chart; } else { - cleanedService.widget.chart = true; + widget.chart = true; } - if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval; - if (pointsLimit) cleanedService.widget.pointsLimit = pointsLimit; - if (diskUnits) cleanedService.widget.diskUnits = diskUnits; + if (refreshInterval) widget.refreshInterval = refreshInterval; + if (pointsLimit) widget.pointsLimit = pointsLimit; + if (diskUnits) widget.diskUnits = diskUnits; } if (type === "mjpeg") { - if (stream) cleanedService.widget.stream = stream; - if (fit) cleanedService.widget.fit = fit; + if (stream) widget.stream = stream; + if (fit) widget.fit = fit; } if (type === "openmediavault") { - if (method) cleanedService.widget.method = method; + if (method) widget.method = method; } if (type === "openwrt") { - if (interfaceName) cleanedService.widget.interfaceName = interfaceName; + if (interfaceName) widget.interfaceName = interfaceName; } if (type === "customapi") { - if (mappings) cleanedService.widget.mappings = mappings; - if (display) cleanedService.widget.display = display; - if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval; + if (mappings) widget.mappings = mappings; + if (display) widget.display = display; + if (refreshInterval) widget.refreshInterval = refreshInterval; } if (type === "calendar") { - if (integrations) cleanedService.widget.integrations = integrations; - if (firstDayInWeek) cleanedService.widget.firstDayInWeek = firstDayInWeek; - if (view) cleanedService.widget.view = view; - if (maxEvents) cleanedService.widget.maxEvents = maxEvents; - if (previousDays) cleanedService.widget.previousDays = previousDays; - if (showTime) cleanedService.widget.showTime = showTime; - if (timezone) cleanedService.widget.timezone = timezone; + if (integrations) widget.integrations = integrations; + if (firstDayInWeek) widget.firstDayInWeek = firstDayInWeek; + if (view) widget.view = view; + if (maxEvents) widget.maxEvents = maxEvents; + if (previousDays) widget.previousDays = previousDays; + if (showTime) widget.showTime = showTime; + if (timezone) widget.timezone = timezone; } if (type === "hdhomerun") { - if (tuner !== undefined) cleanedService.widget.tuner = tuner; + if (tuner !== undefined) widget.tuner = tuner; } if (type === "healthchecks") { - if (uuid !== undefined) cleanedService.widget.uuid = uuid; + if (uuid !== undefined) widget.uuid = uuid; } if (type === "speedtest") { if (bitratePrecision !== undefined) { - cleanedService.widget.bitratePrecision = parseInt(bitratePrecision, 10); + widget.bitratePrecision = parseInt(bitratePrecision, 10); } } if (type === "stocks") { - if (watchlist) cleanedService.widget.watchlist = watchlist; - if (showUSMarketStatus) cleanedService.widget.showUSMarketStatus = showUSMarketStatus; + if (watchlist) widget.watchlist = watchlist; + if (showUSMarketStatus) widget.showUSMarketStatus = showUSMarketStatus; } if (type === "wgeasy") { - if (threshold !== undefined) cleanedService.widget.threshold = parseInt(threshold, 10); + if (threshold !== undefined) widget.threshold = parseInt(threshold, 10); } if (type === "frigate") { - if (enableRecentEvents !== undefined) cleanedService.widget.enableRecentEvents = enableRecentEvents; + if (enableRecentEvents !== undefined) widget.enableRecentEvents = enableRecentEvents; } if (type === "technitium") { - if (range !== undefined) cleanedService.widget.range = range; + if (range !== undefined) widget.range = range; } if (type === "lubelogger") { - if (vehicleID !== undefined) cleanedService.widget.vehicleID = parseInt(vehicleID, 10); + if (vehicleID !== undefined) widget.vehicleID = parseInt(vehicleID, 10); } if (type === "vikunja") { - if (enableTaskList !== undefined) cleanedService.widget.enableTaskList = !!enableTaskList; + if (enableTaskList !== undefined) widget.enableTaskList = !!enableTaskList; } if (type === "prometheusmetric") { - if (metrics) cleanedService.widget.metrics = metrics; - if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval; + if (metrics) widget.metrics = metrics; + if (refreshInterval) widget.refreshInterval = refreshInterval; } if (type === "spoolman") { - if (spoolIds !== undefined) cleanedService.widget.spoolIds = spoolIds; + if (spoolIds !== undefined) widget.spoolIds = spoolIds; } - } - + return widget; + }); return cleanedService; }), })); @@ -693,12 +697,11 @@ export async function getServiceItem(group, service) { return false; } -export default async function getServiceWidget(group, service) { +export default async function getServiceWidget(group, service, index) { const serviceItem = await getServiceItem(group, service); if (serviceItem) { - const { widget } = serviceItem; - return widget; + const { widget, widgets } = serviceItem; + return index > -1 && widgets ? widgets[index] : widget; } - return false; } diff --git a/src/utils/proxy/api-helpers.js b/src/utils/proxy/api-helpers.js index 8e0682db..a02ea623 100644 --- a/src/utils/proxy/api-helpers.js +++ b/src/utils/proxy/api-helpers.js @@ -12,6 +12,7 @@ export function getURLSearchParams(widget, endpoint) { const params = new URLSearchParams({ group: widget.service_group, service: widget.service_name, + index: widget.index, }); if (endpoint) { params.append("endpoint", endpoint); diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index cbe0422a..cea95196 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -9,10 +9,10 @@ import widgets from "widgets/widgets"; const logger = createLogger("credentialedProxyHandler"); export default async function credentialedProxyHandler(req, res, map) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, index } = req.query; if (group && service) { - const widget = await getServiceWidget(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" }); diff --git a/src/utils/proxy/handlers/generic.js b/src/utils/proxy/handlers/generic.js index c6b9236b..2e788a98 100644 --- a/src/utils/proxy/handlers/generic.js +++ b/src/utils/proxy/handlers/generic.js @@ -8,10 +8,10 @@ import widgets from "widgets/widgets"; const logger = createLogger("genericProxyHandler"); export default async function genericProxyHandler(req, res, map) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, index } = req.query; if (group && service) { - const widget = await getServiceWidget(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" }); diff --git a/src/utils/proxy/handlers/jsonrpc.js b/src/utils/proxy/handlers/jsonrpc.js index 3974dbdc..f9fb1883 100644 --- a/src/utils/proxy/handlers/jsonrpc.js +++ b/src/utils/proxy/handlers/jsonrpc.js @@ -65,10 +65,10 @@ export async function sendJsonRpcRequest(url, method, params, widget) { } export default async function jsonrpcProxyHandler(req, res) { - const { group, service, endpoint: method } = req.query; + const { group, service, endpoint: method, index } = req.query; if (group && service) { - const widget = await getServiceWidget(group, service); + const widget = await getServiceWidget(group, service, index); const api = widgets?.[widget.type]?.api; const [, mapping] = Object.entries(widgets?.[widget.type]?.mappings).find(([, value]) => value.endpoint === method); diff --git a/src/utils/proxy/handlers/synology.js b/src/utils/proxy/handlers/synology.js index be44e810..030e53ba 100644 --- a/src/utils/proxy/handlers/synology.js +++ b/src/utils/proxy/handlers/synology.js @@ -131,13 +131,13 @@ function toError(url, synologyError) { } export default async function synologyProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, index } = req.query; if (!group || !service) { return res.status(400).json({ error: "Invalid proxy service type" }); } - const serviceWidget = await getServiceWidget(group, service); + const serviceWidget = await getServiceWidget(group, service, index); const widget = widgets?.[serviceWidget.type]; const mapping = widget?.mappings?.[endpoint]; if (!widget.api || !mapping) { diff --git a/src/widgets/audiobookshelf/proxy.js b/src/widgets/audiobookshelf/proxy.js index 9701c1fe..1a89736b 100644 --- a/src/widgets/audiobookshelf/proxy.js +++ b/src/widgets/audiobookshelf/proxy.js @@ -23,14 +23,14 @@ async function retrieveFromAPI(url, key) { } export default async function audiobookshelfProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, 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); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/beszel/proxy.js b/src/widgets/beszel/proxy.js index 04083e42..61bc969b 100644 --- a/src/widgets/beszel/proxy.js +++ b/src/widgets/beszel/proxy.js @@ -34,10 +34,10 @@ async function login(loginUrl, username, password, service) { } export default async function beszelProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, index } = req.query; if (group && service) { - const widget = await getServiceWidget(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" }); diff --git a/src/widgets/calendar/proxy.js b/src/widgets/calendar/proxy.js index cf754424..d36f30c9 100644 --- a/src/widgets/calendar/proxy.js +++ b/src/widgets/calendar/proxy.js @@ -5,10 +5,10 @@ import createLogger from "utils/logger"; const logger = createLogger("calendarProxyHandler"); export default async function calendarProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, index } = req.query; if (group && service) { - const widget = await getServiceWidget(group, service); + const widget = await getServiceWidget(group, service, index); const integration = widget.integrations?.find((i) => i.name === endpoint); if (integration) { diff --git a/src/widgets/crowdsec/proxy.js b/src/widgets/crowdsec/proxy.js index e78fbc5e..85803845 100644 --- a/src/widgets/crowdsec/proxy.js +++ b/src/widgets/crowdsec/proxy.js @@ -35,14 +35,14 @@ async function login(widget, service) { } export default async function crowdsecProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, index } = req.query; if (!group || !service) { logger.error("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); + const widget = await getServiceWidget(group, service, index); if (!widget || !widgets[widget.type].api) { logger.error("Invalid or missing widget for service '%s' in group '%s'", service, group); return res.status(400).json({ error: "Invalid widget configuration" }); diff --git a/src/widgets/deluge/proxy.js b/src/widgets/deluge/proxy.js index b86873a8..61329697 100644 --- a/src/widgets/deluge/proxy.js +++ b/src/widgets/deluge/proxy.js @@ -40,14 +40,14 @@ function login(url, password) { } export default async function delugeProxyHandler(req, res) { - const { group, service } = req.query; + 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); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/flood/proxy.js b/src/widgets/flood/proxy.js index 3345ad7b..e0c10173 100644 --- a/src/widgets/flood/proxy.js +++ b/src/widgets/flood/proxy.js @@ -28,14 +28,14 @@ async function login(widget) { } export default async function floodProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, 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); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/freshrss/proxy.js b/src/widgets/freshrss/proxy.js index c08e5c87..881094bd 100644 --- a/src/widgets/freshrss/proxy.js +++ b/src/widgets/freshrss/proxy.js @@ -74,14 +74,14 @@ async function apiCall(widget, endpoint, service) { } export default async function freshrssProxyHandler(req, res) { - const { group, service } = req.query; + 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); + 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" }); diff --git a/src/widgets/fritzbox/proxy.js b/src/widgets/fritzbox/proxy.js index a0a22d8b..d1a66d97 100644 --- a/src/widgets/fritzbox/proxy.js +++ b/src/widgets/fritzbox/proxy.js @@ -46,8 +46,8 @@ async function requestEndpoint(apiBaseUrl, service, action) { } export default async function fritzboxProxyHandler(req, res) { - const { group, service } = req.query; - const serviceWidget = await getServiceWidget(group, service); + const { group, service, index } = req.query; + const serviceWidget = await getServiceWidget(group, service, index); if (!serviceWidget) { res.status(500).json({ error: { message: "Service widget not found" } }); diff --git a/src/widgets/gamedig/proxy.js b/src/widgets/gamedig/proxy.js index 05fa615c..ecf6e4c6 100644 --- a/src/widgets/gamedig/proxy.js +++ b/src/widgets/gamedig/proxy.js @@ -7,8 +7,8 @@ const proxyName = "gamedigProxyHandler"; const logger = createLogger(proxyName); export default async function gamedigProxyHandler(req, res) { - const { group, service } = req.query; - const serviceWidget = await getServiceWidget(group, service); + const { group, service, index } = req.query; + const serviceWidget = await getServiceWidget(group, service, index); const url = new URL(serviceWidget.url); try { diff --git a/src/widgets/homeassistant/proxy.js b/src/widgets/homeassistant/proxy.js index fe488f86..e1f02ddb 100644 --- a/src/widgets/homeassistant/proxy.js +++ b/src/widgets/homeassistant/proxy.js @@ -62,14 +62,14 @@ async function getQuery(query, { url, key }) { } export default async function homeassistantProxyHandler(req, res) { - const { group, service } = req.query; + 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); + 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" }); diff --git a/src/widgets/homebox/proxy.js b/src/widgets/homebox/proxy.js index 0d6fdf13..c91ce552 100644 --- a/src/widgets/homebox/proxy.js +++ b/src/widgets/homebox/proxy.js @@ -68,14 +68,14 @@ async function apiCall(widget, endpoint, service) { } export default async function homeboxProxyHandler(req, res) { - const { group, service } = req.query; + 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); + 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" }); diff --git a/src/widgets/homebridge/proxy.js b/src/widgets/homebridge/proxy.js index 17dc8635..4da9197b 100644 --- a/src/widgets/homebridge/proxy.js +++ b/src/widgets/homebridge/proxy.js @@ -71,14 +71,14 @@ async function apiCall(widget, endpoint, service) { } export default async function homebridgeProxyHandler(req, res) { - const { group, service } = req.query; + 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); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/jackett/proxy.js b/src/widgets/jackett/proxy.js index 5292695f..035309b3 100644 --- a/src/widgets/jackett/proxy.js +++ b/src/widgets/jackett/proxy.js @@ -25,14 +25,14 @@ async function fetchJackettCookie(widget, loginURL) { } export default async function jackettProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, index } = req.query; if (!group || !service) { logger.error("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); + const widget = await getServiceWidget(group, service, index); if (!widget || !widgets[widget.type].api) { logger.error("Invalid or missing widget for service '%s' in group '%s'", service, group); return res.status(400).json({ error: "Invalid widget configuration" }); diff --git a/src/widgets/jdownloader/proxy.js b/src/widgets/jdownloader/proxy.js index 88a92d95..ae8c845c 100644 --- a/src/widgets/jdownloader/proxy.js +++ b/src/widgets/jdownloader/proxy.js @@ -12,12 +12,12 @@ const proxyName = "jdownloaderProxyHandler"; const logger = createLogger(proxyName); async function getWidget(req) { - const { group, service } = req.query; + const { group, service, index } = req.query; if (!group || !service) { logger.debug("Invalid or missing service '%s' or group '%s'", service, group); return null; } - const widget = await getServiceWidget(group, service); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); return null; diff --git a/src/widgets/kavita/proxy.js b/src/widgets/kavita/proxy.js index b8e9813f..1c41c45f 100644 --- a/src/widgets/kavita/proxy.js +++ b/src/widgets/kavita/proxy.js @@ -70,14 +70,14 @@ async function apiCall(widget, endpoint, service) { } export default async function KavitaProxyHandler(req, res) { - const { group, service } = req.query; + 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); + 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" }); diff --git a/src/widgets/minecraft/proxy.js b/src/widgets/minecraft/proxy.js index f7bac9d4..98d1be88 100644 --- a/src/widgets/minecraft/proxy.js +++ b/src/widgets/minecraft/proxy.js @@ -7,8 +7,8 @@ const proxyName = "minecraftProxyHandler"; const logger = createLogger(proxyName); export default async function minecraftProxyHandler(req, res) { - const { group, service } = req.query; - const serviceWidget = await getServiceWidget(group, service); + const { group, service, index } = req.query; + const serviceWidget = await getServiceWidget(group, service, index); const url = new URL(serviceWidget.url); try { const pingResponse = await pingWithPromise(url.hostname, url.port || 25565); diff --git a/src/widgets/npm/proxy.js b/src/widgets/npm/proxy.js index 978254f8..6c7ba09e 100644 --- a/src/widgets/npm/proxy.js +++ b/src/widgets/npm/proxy.js @@ -36,10 +36,10 @@ async function login(loginUrl, username, password, service) { } export default async function npmProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, index } = req.query; if (group && service) { - const widget = await getServiceWidget(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" }); diff --git a/src/widgets/omada/proxy.js b/src/widgets/omada/proxy.js index 8e8994a5..f4da1293 100644 --- a/src/widgets/omada/proxy.js +++ b/src/widgets/omada/proxy.js @@ -33,10 +33,10 @@ async function login(loginUrl, username, password, controllerVersionMajor) { } export default async function omadaProxyHandler(req, res) { - const { group, service } = req.query; + const { group, service, index } = req.query; if (group && service) { - const widget = await getServiceWidget(group, service); + const widget = await getServiceWidget(group, service, index); if (widget) { const { url } = widget; diff --git a/src/widgets/openmediavault/proxy.js b/src/widgets/openmediavault/proxy.js index e1f97a56..9cda42e8 100644 --- a/src/widgets/openmediavault/proxy.js +++ b/src/widgets/openmediavault/proxy.js @@ -12,14 +12,14 @@ const BG_POLL_PERIOD = 500; const logger = createLogger(PROXY_NAME); async function getWidget(req) { - const { group, service } = req.query; + const { group, service, index } = req.query; if (!group || !service) { logger.debug("Invalid or missing service '%s' or group '%s'", service, group); return null; } - const widget = await getServiceWidget(group, service); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/openwrt/proxy.js b/src/widgets/openwrt/proxy.js index 977db8ca..0a0da3ff 100644 --- a/src/widgets/openwrt/proxy.js +++ b/src/widgets/openwrt/proxy.js @@ -17,14 +17,14 @@ const PARAMS = { }; async function getWidget(req) { - const { group, service } = req.query; + const { group, service, index } = req.query; if (!group || !service) { logger.debug("Invalid or missing service '%s' or group '%s'", service, group); return null; } - const widget = await getServiceWidget(group, service); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/photoprism/proxy.js b/src/widgets/photoprism/proxy.js index 509bfa0c..fe5096b3 100644 --- a/src/widgets/photoprism/proxy.js +++ b/src/widgets/photoprism/proxy.js @@ -6,14 +6,14 @@ import createLogger from "utils/logger"; const logger = createLogger("photoprismProxyHandler"); export default async function photoprismProxyHandler(req, res) { - const { group, service } = req.query; + 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); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/pihole/proxy.js b/src/widgets/pihole/proxy.js index 35873fa9..bf24624d 100644 --- a/src/widgets/pihole/proxy.js +++ b/src/widgets/pihole/proxy.js @@ -33,7 +33,7 @@ async function login(widget, service) { } export default async function piholeProxyHandler(req, res) { - const { group, service } = req.query; + const { group, service, index } = req.query; let endpoint = "stats/summary"; if (!group || !service) { @@ -41,7 +41,7 @@ export default async function piholeProxyHandler(req, res) { return res.status(400).json({ error: "Invalid proxy service type" }); } - const widget = await getServiceWidget(group, service); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.error("Invalid or missing widget for service '%s' in group '%s'", service, group); return res.status(400).json({ error: "Invalid widget configuration" }); diff --git a/src/widgets/plex/proxy.js b/src/widgets/plex/proxy.js index d8033065..2956f280 100644 --- a/src/widgets/plex/proxy.js +++ b/src/widgets/plex/proxy.js @@ -16,14 +16,14 @@ const tvCacheKey = `${proxyName}__tv`; const logger = createLogger(proxyName); async function getWidget(req) { - const { group, service } = req.query; + const { group, service, index } = req.query; if (!group || !service) { logger.debug("Invalid or missing service '%s' or group '%s'", service, group); return null; } - const widget = await getServiceWidget(group, service); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/pyload/proxy.js b/src/widgets/pyload/proxy.js index d9469d1c..a380c865 100644 --- a/src/widgets/pyload/proxy.js +++ b/src/widgets/pyload/proxy.js @@ -67,11 +67,11 @@ async function login(loginUrl, service, username, password = "") { } export default async function pyloadProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, index } = req.query; try { if (group && service) { - const widget = await getServiceWidget(group, service); + const widget = await getServiceWidget(group, service, index); if (widget) { const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); diff --git a/src/widgets/qbittorrent/proxy.js b/src/widgets/qbittorrent/proxy.js index e1a0f055..aead7582 100644 --- a/src/widgets/qbittorrent/proxy.js +++ b/src/widgets/qbittorrent/proxy.js @@ -21,14 +21,14 @@ async function login(widget) { } export default async function qbittorrentProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, 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); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/qnap/proxy.js b/src/widgets/qnap/proxy.js index 508c8a46..07917d28 100644 --- a/src/widgets/qnap/proxy.js +++ b/src/widgets/qnap/proxy.js @@ -77,14 +77,14 @@ async function apiCall(widget, endpoint, service) { } export default async function qnapProxyHandler(req, res) { - const { group, service } = req.query; + 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); + 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" }); diff --git a/src/widgets/rutorrent/proxy.js b/src/widgets/rutorrent/proxy.js index 47c76191..e0ae44fe 100644 --- a/src/widgets/rutorrent/proxy.js +++ b/src/widgets/rutorrent/proxy.js @@ -45,10 +45,10 @@ const getTorrentInfo = (data) => ({ }); export default async function rutorrentProxyHandler(req, res) { - const { group, service } = req.query; + const { group, service, index } = req.query; if (group && service) { - const widget = await getServiceWidget(group, service); + const widget = await getServiceWidget(group, service, index); if (widget) { const api = widgets?.[widget.type]?.api; diff --git a/src/widgets/suwayomi/proxy.js b/src/widgets/suwayomi/proxy.js index d4d71675..def811cc 100644 --- a/src/widgets/suwayomi/proxy.js +++ b/src/widgets/suwayomi/proxy.js @@ -114,14 +114,14 @@ function extractCounts(responseJSON, fields) { } export default async function suwayomiProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, 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); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/tdarr/proxy.js b/src/widgets/tdarr/proxy.js index 9e26fdc0..88da30fd 100644 --- a/src/widgets/tdarr/proxy.js +++ b/src/widgets/tdarr/proxy.js @@ -8,14 +8,14 @@ const proxyName = "tdarrProxyHandler"; const logger = createLogger(proxyName); export default async function tdarrProxyHandler(req, res) { - const { group, service } = req.query; + 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); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/transmission/proxy.js b/src/widgets/transmission/proxy.js index 823def05..8b8049bc 100644 --- a/src/widgets/transmission/proxy.js +++ b/src/widgets/transmission/proxy.js @@ -11,14 +11,14 @@ const headerCacheKey = `${proxyName}__headers`; const logger = createLogger(proxyName); export default async function transmissionProxyHandler(req, res) { - const { group, service } = req.query; + 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); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/unifi/proxy.js b/src/widgets/unifi/proxy.js index 98c98f37..559065e3 100644 --- a/src/widgets/unifi/proxy.js +++ b/src/widgets/unifi/proxy.js @@ -14,13 +14,13 @@ const prefixCacheKey = `${proxyName}__prefix`; const logger = createLogger(proxyName); async function getWidget(req) { - const { group, service } = req.query; + const { group, service, index } = req.query; let widget = null; if (group === "unifi_console" && service === "unifi_console") { // info widget - const index = req.query?.query ? JSON.parse(req.query.query).index : undefined; - widget = await getPrivateWidgetOptions("unifi_console", index); + const infowidgetIndex = req.query?.query ? JSON.parse(req.query.query).index : undefined; + widget = await getPrivateWidgetOptions("unifi_console", infowidgetIndex); if (!widget) { logger.debug("Error retrieving settings for this Unifi widget"); return null; @@ -32,7 +32,7 @@ async function getWidget(req) { return null; } - widget = await getServiceWidget(group, service); + widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/urbackup/proxy.js b/src/widgets/urbackup/proxy.js index 94b8eeff..4e7a0a8d 100644 --- a/src/widgets/urbackup/proxy.js +++ b/src/widgets/urbackup/proxy.js @@ -3,8 +3,8 @@ import { UrbackupServer } from "urbackup-server-api"; import getServiceWidget from "utils/config/service-helpers"; export default async function urbackupProxyHandler(req, res) { - const { group, service } = req.query; - const serviceWidget = await getServiceWidget(group, service); + const { group, service, index } = req.query; + const serviceWidget = await getServiceWidget(group, service, index); const server = new UrbackupServer({ url: serviceWidget.url, diff --git a/src/widgets/watchtower/proxy.js b/src/widgets/watchtower/proxy.js index b3155a1e..588d08ee 100644 --- a/src/widgets/watchtower/proxy.js +++ b/src/widgets/watchtower/proxy.js @@ -8,14 +8,14 @@ const proxyName = "watchtowerProxyHandler"; const logger = createLogger(proxyName); export default async function watchtowerProxyHandler(req, res) { - const { group, service, endpoint } = req.query; + const { group, service, endpoint, 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); + const widget = await getServiceWidget(group, service, index); if (!widget) { logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); diff --git a/src/widgets/xteve/proxy.js b/src/widgets/xteve/proxy.js index 421f2b49..453e3645 100644 --- a/src/widgets/xteve/proxy.js +++ b/src/widgets/xteve/proxy.js @@ -7,13 +7,13 @@ import getServiceWidget from "utils/config/service-helpers"; const logger = createLogger("xteveProxyHandler"); export default async function xteveProxyHandler(req, res) { - const { group, service } = req.query; + const { group, service, index } = req.query; if (!group || !service) { return res.status(400).json({ error: "Invalid proxy service type" }); } - const widget = await getServiceWidget(group, service); + const widget = await getServiceWidget(group, service, index); const api = widgets?.[widget.type]?.api; if (!api) { return res.status(403).json({ error: "Service does not support API calls" }); From be8363cc3514749f54c5060c6548138129612e82 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:01:47 -0800 Subject: [PATCH 05/66] Feature: nested groups (#4346) --- src/components/services/group.jsx | 33 ++++++++-- src/components/services/item.jsx | 6 +- src/components/services/list.jsx | 4 +- src/components/services/ping.jsx | 4 +- src/components/services/site-monitor.jsx | 4 +- src/pages/api/ping.js | 6 +- src/pages/api/siteMonitor.js | 6 +- src/pages/index.jsx | 6 +- src/utils/config/api-response.js | 24 +++++-- src/utils/config/service-helpers.js | 81 ++++++++++++++++-------- 10 files changed, 119 insertions(+), 55 deletions(-) diff --git a/src/components/services/group.jsx b/src/components/services/group.jsx index cdbb89f3..f25f7ec1 100644 --- a/src/components/services/group.jsx +++ b/src/components/services/group.jsx @@ -3,12 +3,13 @@ import classNames from "classnames"; import { Disclosure, Transition } from "@headlessui/react"; import { MdKeyboardArrowDown } from "react-icons/md"; +import { columnMap } from "../../utils/layout/columns"; + import List from "components/services/list"; import ResolvedIcon from "components/resolvedicon"; export default function ServicesGroup({ group, - services, layout, fiveColumns, disableCollapse, @@ -23,7 +24,7 @@ export default function ServicesGroup({ return (
)}

- {services.name} + {group.name}

- + + {group.groups?.length > 0 && ( +
+ {group.groups.map((subgroup) => ( + + ))} +
+ )}
diff --git a/src/components/services/item.jsx b/src/components/services/item.jsx index 54560d6f..adf5fc97 100644 --- a/src/components/services/item.jsx +++ b/src/components/services/item.jsx @@ -12,7 +12,7 @@ import Kubernetes from "widgets/kubernetes/component"; import { SettingsContext } from "utils/contexts/settings"; import ResolvedIcon from "components/resolvedicon"; -export default function Item({ service, group, useEqualHeights }) { +export default function Item({ service, groupName, useEqualHeights }) { const hasLink = service.href && service.href !== "#"; const { settings } = useContext(SettingsContext); const showStats = service.showStats === false ? false : settings.showStats; @@ -90,14 +90,14 @@ export default function Item({ service, group, useEqualHeights }) { > {service.ping && (
- + Ping status
)} {service.siteMonitor && (
- + Site monitor status
)} diff --git a/src/components/services/list.jsx b/src/components/services/list.jsx index f3fd6e2a..c15d6aed 100644 --- a/src/components/services/list.jsx +++ b/src/components/services/list.jsx @@ -4,7 +4,7 @@ import { columnMap } from "../../utils/layout/columns"; import Item from "components/services/item"; -export default function List({ group, services, layout, useEqualHeights }) { +export default function List({ groupName, services, layout, useEqualHeights }) { return (
    s).join("-")} service={service} - group={group} + groupName={groupName} useEqualHeights={layout?.useEqualHeights ?? useEqualHeights} /> ))} diff --git a/src/components/services/ping.jsx b/src/components/services/ping.jsx index f72d40b3..670f9d4b 100644 --- a/src/components/services/ping.jsx +++ b/src/components/services/ping.jsx @@ -1,9 +1,9 @@ import { useTranslation } from "react-i18next"; import useSWR from "swr"; -export default function Ping({ group, service, style }) { +export default function Ping({ groupName, serviceName, style }) { const { t } = useTranslation(); - const { data, error } = useSWR(`/api/ping?${new URLSearchParams({ group, service }).toString()}`, { + const { data, error } = useSWR(`/api/ping?${new URLSearchParams({ groupName, serviceName }).toString()}`, { refreshInterval: 30000, }); diff --git a/src/components/services/site-monitor.jsx b/src/components/services/site-monitor.jsx index 3d5ef79e..4dceb44c 100644 --- a/src/components/services/site-monitor.jsx +++ b/src/components/services/site-monitor.jsx @@ -1,9 +1,9 @@ import { useTranslation } from "react-i18next"; import useSWR from "swr"; -export default function SiteMonitor({ group, service, style }) { +export default function SiteMonitor({ groupName, serviceName, style }) { const { t } = useTranslation(); - const { data, error } = useSWR(`/api/siteMonitor?${new URLSearchParams({ group, service }).toString()}`, { + const { data, error } = useSWR(`/api/siteMonitor?${new URLSearchParams({ groupName, serviceName }).toString()}`, { refreshInterval: 30000, }); diff --git a/src/pages/api/ping.js b/src/pages/api/ping.js index e540fa68..8ef64ffc 100644 --- a/src/pages/api/ping.js +++ b/src/pages/api/ping.js @@ -6,10 +6,10 @@ import createLogger from "utils/logger"; const logger = createLogger("ping"); export default async function handler(req, res) { - const { group, service } = req.query; - const serviceItem = await getServiceItem(group, service); + const { groupName, serviceName } = req.query; + const serviceItem = await getServiceItem(groupName, serviceName); if (!serviceItem) { - logger.debug(`No service item found for group ${group} named ${service}`); + logger.debug(`No service item found for group ${groupName} named ${serviceName}`); return res.status(400).send({ error: "Unable to find service, see log for details.", }); diff --git a/src/pages/api/siteMonitor.js b/src/pages/api/siteMonitor.js index 9e030d74..072d3d4c 100644 --- a/src/pages/api/siteMonitor.js +++ b/src/pages/api/siteMonitor.js @@ -7,10 +7,10 @@ import { httpProxy } from "utils/proxy/http"; const logger = createLogger("siteMonitor"); export default async function handler(req, res) { - const { group, service } = req.query; - const serviceItem = await getServiceItem(group, service); + const { groupName, serviceName } = req.query; + const serviceItem = await getServiceItem(groupName, serviceName); if (!serviceItem) { - logger.debug(`No service item found for group ${group} named ${service}`); + logger.debug(`No service item found for group ${groupName} named ${serviceName}`); return res.status(400).send({ error: "Unable to find service, see log for details.", }); diff --git a/src/pages/index.jsx b/src/pages/index.jsx index dd0df95f..7a7fdef0 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -291,8 +291,7 @@ function Home({ initialSettings }) { group.services ? ( ( { + if (group.name === mergedGroup.name) { + // eslint-disable-next-line no-param-reassign + group.services = mergedGroup.services; + } else if (group.groups) { + mergeSubgroups(group.groups, mergedGroup); + } + }); +} + export async function servicesResponse() { let discoveredDockerServices; let discoveredKubernetesServices; @@ -140,25 +152,29 @@ export async function servicesResponse() { const definedLayouts = initialSettings.layout ? Object.keys(initialSettings.layout) : null; mergedGroupsNames.forEach((groupName) => { - const discoveredDockerGroup = discoveredDockerServices.find((group) => group.name === groupName) || { + const discoveredDockerGroup = findGroupByName(discoveredDockerServices, groupName) || { services: [], }; - const discoveredKubernetesGroup = discoveredKubernetesServices.find((group) => group.name === groupName) || { + const discoveredKubernetesGroup = findGroupByName(discoveredKubernetesServices, groupName) || { services: [], }; - const configuredGroup = configuredServices.find((group) => group.name === groupName) || { services: [] }; + const configuredGroup = findGroupByName(configuredServices, groupName) || { services: [] }; const mergedGroup = { name: groupName, services: [...discoveredDockerGroup.services, ...discoveredKubernetesGroup.services, ...configuredGroup.services] .filter((service) => service) .sort(compareServices), + groups: [...configuredGroup.groups], }; if (definedLayouts) { const layoutIndex = definedLayouts.findIndex((layout) => layout === mergedGroup.name); if (layoutIndex > -1) sortedGroups[layoutIndex] = mergedGroup; - else unsortedGroups.push(mergedGroup); + else if (configuredGroup.name) { + // this is a nested group, so find the parent group and merge the services + mergeSubgroups(configuredServices, mergedGroup); + } else unsortedGroups.push(mergedGroup); } else { unsortedGroups.push(mergedGroup); } diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index e6ef6173..0f1e2c8c 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -13,6 +13,38 @@ import * as shvl from "utils/config/shvl"; const logger = createLogger("service-helpers"); +function parseServicesToGroups(services) { + if (!services) { + return []; + } + + // map easy to write YAML objects into easy to consume JS arrays + return services.map((serviceGroup) => { + const name = Object.keys(serviceGroup)[0]; + let groups = []; + const serviceGroupServices = []; + serviceGroup[name].forEach((entries) => { + const entryName = Object.keys(entries)[0]; + if (Array.isArray(entries[entryName])) { + groups = groups.concat(parseServicesToGroups([{ [entryName]: entries[entryName] }])); + } else { + serviceGroupServices.push({ + name: entryName, + ...entries[entryName], + weight: entries[entryName].weight || serviceGroupServices.length * 100, // default weight + type: "service", + }); + } + }); + return { + name, + type: "group", + services: serviceGroupServices, + groups, + }; + }); +} + export async function servicesFromConfig() { checkAndCopyConfig("services.yaml"); @@ -20,31 +52,7 @@ export async function servicesFromConfig() { const rawFileContents = await fs.readFile(servicesYaml, "utf8"); const fileContents = substituteEnvironmentVars(rawFileContents); const services = yaml.load(fileContents); - - if (!services) { - return []; - } - - // map easy to write YAML objects into easy to consume JS arrays - const servicesArray = services.map((servicesGroup) => ({ - name: Object.keys(servicesGroup)[0], - services: servicesGroup[Object.keys(servicesGroup)[0]].map((entries) => ({ - name: Object.keys(entries)[0], - ...entries[Object.keys(entries)[0]], - type: "service", - })), - })); - - // add default weight to services based on their position in the configuration - servicesArray.forEach((group, groupIndex) => { - group.services.forEach((service, serviceIndex) => { - if (service.weight === undefined) { - servicesArray[groupIndex].services[serviceIndex].weight = (serviceIndex + 1) * 100; - } - }); - }); - - return servicesArray; + return parseServicesToGroups(services); } export async function servicesFromDocker() { @@ -667,13 +675,30 @@ export function cleanServiceGroups(groups) { }); return cleanedService; }), + type: serviceGroup.type || "group", + groups: serviceGroup.groups ? cleanServiceGroups(serviceGroup.groups) : [], })); } +export function findGroupByName(groups, name) { + for (let i = 0; i < groups.length; i += 1) { + const group = groups[i]; + if (group.name === name) { + return group; + } else if (group.groups) { + const foundGroup = findGroupByName(group.groups, name); + if (foundGroup) { + return foundGroup; + } + } + } + return null; +} + export async function getServiceItem(group, service) { const configuredServices = await servicesFromConfig(); - const serviceGroup = configuredServices.find((g) => g.name === group); + const serviceGroup = findGroupByName(configuredServices, group); if (serviceGroup) { const serviceEntry = serviceGroup.services.find((s) => s.name === service); if (serviceEntry) return serviceEntry; @@ -681,14 +706,14 @@ export async function getServiceItem(group, service) { const discoveredServices = await servicesFromDocker(); - const dockerServiceGroup = discoveredServices.find((g) => g.name === group); + const dockerServiceGroup = findGroupByName(discoveredServices, group); if (dockerServiceGroup) { const dockerServiceEntry = dockerServiceGroup.services.find((s) => s.name === service); if (dockerServiceEntry) return dockerServiceEntry; } const kubernetesServices = await servicesFromKubernetes(); - const kubernetesServiceGroup = kubernetesServices.find((g) => g.name === group); + const kubernetesServiceGroup = findGroupByName(kubernetesServices, group); if (kubernetesServiceGroup) { const kubernetesServiceEntry = kubernetesServiceGroup.services.find((s) => s.name === service); if (kubernetesServiceEntry) return kubernetesServiceEntry; From aaf4a3e92f2e3bc6933f3a2699c3b764818ff5bd Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 27 Nov 2024 17:59:15 -0800 Subject: [PATCH 06/66] Add note --- src/utils/config/service-helpers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 0f1e2c8c..e3c491ac 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -681,6 +681,7 @@ export function cleanServiceGroups(groups) { } export function findGroupByName(groups, name) { + // Deep search for a group by name. Using for loop allows for early return for (let i = 0; i < groups.length; i += 1) { const group = groups[i]; if (group.name === name) { From 230da3d2ebddb9031f601a871a7483430500617f Mon Sep 17 00:00:00 2001 From: DamitusThyYeetus123 <108782125+DamitusThyYeetus123@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:45:30 +1100 Subject: [PATCH 07/66] Enhancement: support hrefs for info widgets (#4347) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- src/components/widgets/widget/container.jsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/widgets/widget/container.jsx b/src/components/widgets/widget/container.jsx index c9240dd3..4a1fdd37 100644 --- a/src/components/widgets/widget/container.jsx +++ b/src/components/widgets/widget/container.jsx @@ -1,10 +1,13 @@ 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"; + export function getAllClasses(options, additionalClassNames = "") { if (options?.style?.header === "boxedWidgets") { if (options?.style?.cardBlur !== undefined) { @@ -56,7 +59,17 @@ export function getBottomBlock(children) { } export default function Container({ children = [], options, additionalClassNames = "" }) { - return ( + const { settings } = useContext(SettingsContext); + return options.href ? ( + + {getInnerBlock(children)} + {getBottomBlock(children)} + + ) : (
    {getInnerBlock(children)} {getBottomBlock(children)} From 6d829bce7993795ecaa7c89206afb904948f4cb5 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 27 Nov 2024 22:12:59 -0800 Subject: [PATCH 08/66] Enhancement: use css color-scheme (#4349) --- src/pages/index.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 7a7fdef0..3f8ebf86 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -452,6 +452,7 @@ function Home({ initialSettings }) { } export default function Wrapper({ initialSettings, fallback }) { + const { theme } = useContext(ThemeContext); const wrappedStyle = {}; let backgroundBlur = false; let backgroundSaturate = false; @@ -482,8 +483,9 @@ export default function Wrapper({ initialSettings, fallback }) { id="page_wrapper" className={classNames( "relative", - initialSettings.theme && initialSettings.theme, + theme && theme, initialSettings.color && `theme-${initialSettings.color}`, + theme === "dark" ? "scheme-dark" : "scheme-light", )} >
    Date: Wed, 27 Nov 2024 22:49:14 -0800 Subject: [PATCH 09/66] Documentation: doc updates for nesting, reorganizing, fixes --- docs/configs/info-widgets.md | 24 +++++++++++ docs/configs/service-widgets.md | 58 ------------------------- docs/configs/services.md | 71 +++++++++++++++++++++++++++++++ docs/configs/settings.md | 29 +++++++++++-- docs/troubleshooting/index.md | 2 +- docs/widgets/authoring/proxies.md | 2 +- docs/widgets/index.md | 4 ++ docs/widgets/info/openmeteo.md | 2 +- docs/widgets/info/weather.md | 22 ---------- mkdocs.yml | 4 +- src/skeleton/services.yaml | 2 +- src/skeleton/settings.yaml | 2 +- src/skeleton/widgets.yaml | 2 +- 13 files changed, 132 insertions(+), 92 deletions(-) create mode 100644 docs/configs/info-widgets.md delete mode 100644 docs/configs/service-widgets.md delete mode 100644 docs/widgets/info/weather.md diff --git a/docs/configs/info-widgets.md b/docs/configs/info-widgets.md new file mode 100644 index 00000000..76314396 --- /dev/null +++ b/docs/configs/info-widgets.md @@ -0,0 +1,24 @@ +--- +title: Information Widgets +description: Homepage info widgets. +--- + +Information widgets are widgets that provide information about your system or environment and are displayed at the top of the homepage. You can find a list of all available info widgets under the [Info Widgets](../widgets/info/index.md) section. + +Info widgets are defined in the widgets.yaml + +Each widget has its own configuration options, which are detailed in the widget's documentation. + +## Layout + +Info widgets are displayed in the order they are defined in the `widgets.yaml` file. You can change the order by moving the widgets around in the file. However, some widgets (weather, search and datetime) are aligned to the right side of the screen which can affect the layout of the widgets. + +## Adding A Link + +You can add a link to an info widget such as the logo or text widgets by adding an `href` option, for example: + +```yaml +logo: + href: https://example.com + target: _blank # Optional, can be set in settings +``` diff --git a/docs/configs/service-widgets.md b/docs/configs/service-widgets.md deleted file mode 100644 index df696f61..00000000 --- a/docs/configs/service-widgets.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Service Widgets -description: Service Widget Configuration ---- - -Unless otherwise noted, URLs should not end with a `/` or other API path. Each widget will handle the path on its own. - -Each service can have widgets attached to it (often matching the service type, but that's not forced). - -In addition to the href of the service, you can also specify the target location in which to open that link. See [Link Target](settings.md#link-target) for more details. - -Using Emby as an example, this is how you would attach the Emby service widget. - -```yaml -- Emby: - icon: emby.png - href: http://emby.host.or.ip/ - description: Movies & TV Shows - widget: - type: emby - url: http://emby.host.or.ip - key: apikeyapikeyapikeyapikeyapikey -``` - -## Multiple Widgets - -Each service can have multiple widgets attached to it, for example: - -```yaml -- Emby: - icon: emby.png - href: http://emby.host.or.ip/ - description: Movies & TV Shows - widgets: - - type: emby - url: http://emby.host.or.ip - key: apikeyapikeyapikeyapikeyapikey - - type: uptimekuma - url: http://uptimekuma.host.or.ip:port - slug: statuspageslug -``` - -## Field Visibility - -Each widget can optionally provide a list of which fields should be visible via the `fields` widget property. If no fields are specified, then all fields will be displayed. The `fields` property must be a valid YAML array of strings. As an example, here is the entry for Sonarr showing only a couple of fields. - -**In all cases a widget will work and display all fields without specifying the `fields` property.** - -```yaml -- Sonarr: - icon: sonarr.png - href: http://sonarr.host.or.ip - widget: - type: sonarr - fields: ["wanted", "queued"] - url: http://sonarr.host.or.ip - key: apikeyapikeyapikeyapikeyapikey -``` diff --git a/docs/configs/services.md b/docs/configs/services.md index 9cb75177..6ef25c39 100644 --- a/docs/configs/services.md +++ b/docs/configs/services.md @@ -21,6 +21,23 @@ Groups are defined as top-level array entries. Service Groups +### Nested Groups + +Groups can be nested by using the same format as the top-level groups. + +```yaml +- Group A: + - Service A: + href: http://localhost/ + + - Group B: + - Service B: + href: http://localhost/ + + - Service C: + href: http://localhost/ +``` + ## Services Services are defined as array entries on groups, @@ -43,6 +60,60 @@ Services are defined as array entries on groups, Service Services +### Service Widgets + +Each service can have widgets attached to it (often matching the service type, but that's not forced). + +In addition to the href of the service, you can also specify the target location in which to open that link. See [Link Target](settings.md#link-target) for more details. + +Using Emby as an example, this is how you would attach the Emby service widget. + +```yaml +- Emby: + icon: emby.png + href: http://emby.host.or.ip/ + description: Movies & TV Shows + widget: + type: emby + url: http://emby.host.or.ip + key: apikeyapikeyapikeyapikeyapikey +``` + +#### Multiple Widgets + +Each service can have multiple widgets attached to it, for example: + +```yaml +- Emby: + icon: emby.png + href: http://emby.host.or.ip/ + description: Movies & TV Shows + widgets: + - type: emby + url: http://emby.host.or.ip + key: apikeyapikeyapikeyapikeyapikey + - type: uptimekuma + url: http://uptimekuma.host.or.ip:port + slug: statuspageslug +``` + +#### Field Visibility + +Each widget can optionally provide a list of which fields should be visible via the `fields` widget property. If no fields are specified, then all fields will be displayed. The `fields` property must be a valid YAML array of strings. As an example, here is the entry for Sonarr showing only a couple of fields. + +**In all cases a widget will work and display all fields without specifying the `fields` property.** + +```yaml +- Sonarr: + icon: sonarr.png + href: http://sonarr.host.or.ip + widget: + type: sonarr + fields: ["wanted", "queued"] + url: http://sonarr.host.or.ip + key: apikeyapikeyapikeyapikeyapikey +``` + ## Descriptions Services may have descriptions, diff --git a/docs/configs/settings.md b/docs/configs/settings.md index 2f387a65..7e1815bb 100644 --- a/docs/configs/settings.md +++ b/docs/configs/settings.md @@ -137,6 +137,27 @@ layout: columns: 3 ``` +### Nested Groups + +If your services config has nested groups, you can apply settings to these groups by nesting them in the layout block +and using the same settings. For example + +```yaml +layout: + Group A: + style: row + columns: 4 + Group C: + style: row + columns: 2 + Nested Group A: + style: row + columns: 2 + Nested Group B: + style: row + columns: 2 +``` + ### Headers You can hide headers for each section in the layout as well by passing `header` as false, like so: @@ -348,12 +369,12 @@ This can also be set for individual services. Note setting this at the service l ## Providers -The `providers` section allows you to define shared API provider options and secrets. Currently this allows you to define your weather API keys in secret and is also the location of the Longhorn URL and credentials. +The `providers` section allows you to define shared API provider options and secrets. ```yaml providers: openweathermap: openweathermapapikey - weatherapi: weatherapiapikey + finnhub: yourfinnhubapikeyhere longhorn: url: https://longhorn.example.com username: admin @@ -363,10 +384,10 @@ providers: You can then pass `provider` instead of `apiKey` in your widget configuration. ```yaml -- weatherapi: +- openweathermap: latitude: 50.449684 longitude: 30.525026 - provider: weatherapi + provider: openweathermap ``` ## Quick Launch diff --git a/docs/troubleshooting/index.md b/docs/troubleshooting/index.md index 82a7381c..bbde4cf3 100644 --- a/docs/troubleshooting/index.md +++ b/docs/troubleshooting/index.md @@ -17,7 +17,7 @@ hide: All service widgets work essentially the same, that is, homepage makes a proxied call to an API made available by that service. The majority of the time widgets don't work it is a configuration issue. Of course, sometimes things do break. Some basic steps to try: -1. Ensure that you follow the rule mentioned on https://gethomepage.dev/configs/service-widgets/. **Unless otherwise noted, URLs should not end with a / or other API path. Each widget will handle the path on its own.**. This is very important as including a trailing slash can result in an error. +1. **URLs should not end with a / or other API path. Each widget will handle the path on its own.**. Including a trailing slash can result in an error. 2. Verify the homepage installation can connect to the IP address or host you are using for the widget `url`. This is most simply achieved by pinging the server from the homepage machine, in Docker this means _from inside the container_ itself, e.g.: diff --git a/docs/widgets/authoring/proxies.md b/docs/widgets/authoring/proxies.md index 15cdb670..a8b8073e 100644 --- a/docs/widgets/authoring/proxies.md +++ b/docs/widgets/authoring/proxies.md @@ -50,7 +50,7 @@ You can also pass API keys from the widget configuration to the proxy handler, f ### `credentialedProxyHandler` -A proxy handler that makes authenticated by setting request headers. Credentials are pulled from the widgets configuration. +A proxy handler that makes authenticated requests by setting request headers. Credentials are pulled from the widgets configuration. By default the key is passed as an `X-API-Key` header. If you need to pass the key as something else, either add a case to the credentialedProxyHandler or create a new proxy handler. diff --git a/docs/widgets/index.md b/docs/widgets/index.md index 4bd45af7..fbb8edc6 100644 --- a/docs/widgets/index.md +++ b/docs/widgets/index.md @@ -28,6 +28,8 @@ Service widgets are used to display the status of a service, often a web service slug: aaaaaaabbbbb ``` +More detail on configuring service widgets can be found in the [Service Widgets Config](../configs/services.md) section. + ## Info Widgets Info widgets are used to display information in the header, often about your system or environment. Info widgets are defined your `widgets.yaml` file. Here's an example: @@ -39,3 +41,5 @@ Info widgets are used to display information in the header, often about your sys longitude: -117.51 cache: 5 ``` + +More detail on configuring info widgets can be found in the [Info Widgets Config](../configs/info-widgets.md) section. diff --git a/docs/widgets/info/openmeteo.md b/docs/widgets/info/openmeteo.md index fb5bb171..ec84ab17 100644 --- a/docs/widgets/info/openmeteo.md +++ b/docs/widgets/info/openmeteo.md @@ -3,7 +3,7 @@ title: Open-Meteo description: Open-Meteo Information Widget Configuration --- -No registration is required at all! See [https://open-meteo.com/en/docs](https://open-meteo.com/en/docs) +Homepage's recommended weather widget. No registration is required at all! See [https://open-meteo.com/en/docs](https://open-meteo.com/en/docs) ```yaml - openmeteo: diff --git a/docs/widgets/info/weather.md b/docs/widgets/info/weather.md deleted file mode 100644 index ab13b673..00000000 --- a/docs/widgets/info/weather.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Weather API -description: Weather API Information Widget Configuration ---- - -**Note: this widget is considered 'deprecated' since there is no longer a free Weather API tier for new members. See the openmeteo or openweathermap widgets for alternatives.** - -The free tier is all that's required, you will need to [register](https://www.weatherapi.com/signup.aspx) and grab your API key. - -```yaml -- weatherapi: - label: Kyiv # optional - latitude: 50.449684 - longitude: 30.525026 - units: metric # or imperial - apiKey: yourweatherapikey - cache: 5 # Time in minutes to cache API responses, to stay within limits - format: # optional, Intl.NumberFormat options - maximumFractionDigits: 1 -``` - -You can optionally not pass a `latitude` and `longitude` and the widget will use your current location (requires a secure context, eg. HTTPS). diff --git a/mkdocs.yml b/mkdocs.yml index a19d3b83..fa2188ad 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,8 +21,8 @@ nav: - configs/index.md - configs/settings.md - configs/bookmarks.md + - configs/info-widgets.md - configs/services.md - - configs/service-widgets.md - configs/kubernetes.md - configs/docker.md - configs/custom-css-js.md @@ -142,6 +142,7 @@ nav: - widgets/services/spoolman.md - widgets/services/stash.md - widgets/services/stocks.md + - widgets/services/suwayomi.md - widgets/services/swagdashboard.md - widgets/services/syncthing-relay-server.md - widgets/services/tailscale.md @@ -177,7 +178,6 @@ nav: - widgets/info/search.md - widgets/info/stocks.md - widgets/info/unifi_controller.md - - widgets/info/weather.md - "Learn": - widgets/authoring/index.md - "Getting Started": widgets/authoring/getting-started.md diff --git a/src/skeleton/services.yaml b/src/skeleton/services.yaml index 77626b1c..39b37926 100644 --- a/src/skeleton/services.yaml +++ b/src/skeleton/services.yaml @@ -1,6 +1,6 @@ --- # For configuration options and examples, please see: -# https://gethomepage.dev/configs/services +# https://gethomepage.dev/configs/services/ - My First Group: - My First Service: diff --git a/src/skeleton/settings.yaml b/src/skeleton/settings.yaml index 141053f5..2e828c08 100644 --- a/src/skeleton/settings.yaml +++ b/src/skeleton/settings.yaml @@ -1,6 +1,6 @@ --- # For configuration options and examples, please see: -# https://gethomepage.dev/configs/settings +# https://gethomepage.dev/configs/settings/ providers: openweathermap: openweathermapapikey diff --git a/src/skeleton/widgets.yaml b/src/skeleton/widgets.yaml index 23c8d613..b1cf0f55 100644 --- a/src/skeleton/widgets.yaml +++ b/src/skeleton/widgets.yaml @@ -1,6 +1,6 @@ --- # For configuration options and examples, please see: -# https://gethomepage.dev/configs/service-widgets +# https://gethomepage.dev/configs/info-widgets/ - resources: cpu: true From 5cc487a96ddd6327d2d7a0d6acfabc3d5f960e59 Mon Sep 17 00:00:00 2001 From: zombaru <16330202+zombaru@users.noreply.github.com> Date: Thu, 28 Nov 2024 20:15:28 -0800 Subject: [PATCH 10/66] Documentation: Add missing admonition type to UniFi docs (#4353) --- docs/widgets/info/unifi_controller.md | 2 +- docs/widgets/services/unifi-controller.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/widgets/info/unifi_controller.md b/docs/widgets/info/unifi_controller.md index b77d8ed0..e16ca40b 100644 --- a/docs/widgets/info/unifi_controller.md +++ b/docs/widgets/info/unifi_controller.md @@ -7,7 +7,7 @@ _(Find the Unifi Controller service widget [here](../services/unifi-controller.m You can display general connectivity status from your Unifi (Network) Controller. -!!! +!!! warning When authenticating you will want to use a local account that has at least read privileges. diff --git a/docs/widgets/services/unifi-controller.md b/docs/widgets/services/unifi-controller.md index d137c2a9..c5efc688 100644 --- a/docs/widgets/services/unifi-controller.md +++ b/docs/widgets/services/unifi-controller.md @@ -9,7 +9,7 @@ _(Find the Unifi Controller information widget [here](../info/unifi_controller.m You can display general connectivity status from your Unifi (Network) Controller. -!!! +!!! warning When authenticating you will want to use a local account that has at least read privileges. From 276a1c3ef423f7b027a84ad5c8ac7ef30d04ca2f Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 28 Nov 2024 21:54:22 -0800 Subject: [PATCH 11/66] Chore: better tailscale error handling --- src/widgets/tailscale/component.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widgets/tailscale/component.jsx b/src/widgets/tailscale/component.jsx index 3929b2ed..d3c937d5 100644 --- a/src/widgets/tailscale/component.jsx +++ b/src/widgets/tailscale/component.jsx @@ -11,8 +11,8 @@ export default function Component({ service }) { const { data: statsData, error: statsError } = useWidgetAPI(widget, "device"); - if (statsError) { - return ; + if (statsError || statsData?.message) { + return ; } if (!statsData) { From a28952ce698e330c9cfef55289acf8c5cc30efe6 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Fri, 29 Nov 2024 10:14:53 -0800 Subject: [PATCH 12/66] Chore: move custom css loading, add letter-spacing (#4359) --- src/pages/_document.jsx | 2 ++ src/pages/index.jsx | 2 -- src/styles/globals.css | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx index bfe3fc93..e69ca007 100644 --- a/src/pages/_document.jsx +++ b/src/pages/_document.jsx @@ -10,6 +10,8 @@ export default function Document() { /> + + {/* eslint-disable-line @next/next/no-css-tags */}
    diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 3f8ebf86..0bdc78b6 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -374,8 +374,6 @@ function Home({ initialSettings }) { )} - - {/* eslint-disable-line @next/next/no-css-tags */}