mirror of
https://github.com/karl0ss/homepage.git
synced 2025-04-29 12:03:41 +01:00
Merge branch 'dev'
This commit is contained in:
commit
1656f02418
35
.github/workflows/docker-publish.yml
vendored
35
.github/workflows/docker-publish.yml
vendored
@ -26,8 +26,6 @@ on:
|
|||||||
merge_group:
|
merge_group:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Use docker.io for Docker Hub if empty
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
# github.repository as <account>/<repo>
|
# github.repository as <account>/<repo>
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
IMAGE_NAME: ${{ github.repository }}
|
||||||
|
|
||||||
@ -66,14 +64,6 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Install the cosign tool except on PR
|
|
||||||
# https://github.com/sigstore/cosign-installer
|
|
||||||
- name: Install cosign
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
uses: sigstore/cosign-installer@main
|
|
||||||
with:
|
|
||||||
cosign-release: 'v1.13.1' # optional
|
|
||||||
|
|
||||||
# Setup QEMU
|
# Setup QEMU
|
||||||
# https://github.com/marketplace/actions/docker-setup-buildx#with-qemu
|
# https://github.com/marketplace/actions/docker-setup-buildx#with-qemu
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
@ -99,9 +89,15 @@ jobs:
|
|||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
# Extract metadata (tags, labels) for Docker
|
# Extract metadata (tags, labels) for Docker
|
||||||
# https://github.com/docker/metadata-action
|
# https://github.com/docker/metadata-action
|
||||||
@ -109,7 +105,9 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: |
|
||||||
|
${{ env.IMAGE_NAME }}
|
||||||
|
ghcr.io/${{ env.IMAGE_NAME }}
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=auto
|
latest=auto
|
||||||
|
|
||||||
@ -133,19 +131,6 @@ jobs:
|
|||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||||
|
|
||||||
# Sign the resulting Docker image digest except on PRs.
|
|
||||||
# This will only write to the public Rekor transparency log when the Docker
|
|
||||||
# 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 }}
|
|
||||||
|
|
||||||
# Temp fix
|
# Temp fix
|
||||||
# https://github.com/docker/build-push-action/issues/252
|
# https://github.com/docker/build-push-action/issues/252
|
||||||
# https://github.com/moby/buildkit/issues/1896
|
# https://github.com/moby/buildkit/issues/1896
|
||||||
|
20
.github/workflows/reaction-comments.yml
vendored
Normal file
20
.github/workflows/reaction-comments.yml
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
name: 'Reaction Comments'
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created, edited]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created, edited]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
action:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/reaction-comments@v4
|
@ -100,6 +100,8 @@ If you are using multiple instances of homepage, an `instance` annotation can be
|
|||||||
|
|
||||||
If you have a single service that needs to be shown on multiple specific instances of homepage (but not on all of them), the service can be annotated by multiple `instance.name` annotations, where `name` can be the names of your specific multiple homepage instances. For example, a service that is annotated with `gethomepage.dev/instance.public: ""` and `gethomepage.dev/instance.internal: ""` will be shown on `public` and `internal` homepage instances.
|
If you have a single service that needs to be shown on multiple specific instances of homepage (but not on all of them), the service can be annotated by multiple `instance.name` annotations, where `name` can be the names of your specific multiple homepage instances. For example, a service that is annotated with `gethomepage.dev/instance.public: ""` and `gethomepage.dev/instance.internal: ""` will be shown on `public` and `internal` homepage instances.
|
||||||
|
|
||||||
|
Use the `gethomepage.dev/pod-selector` selector to specify the pod used for the health check. For example, a service that is annotated with `gethomepage.dev/pod-selector: app.kubernetes.io/name=deployment` would link to a pod with the label `app.kubernetes.io/name: deployment`.
|
||||||
|
|
||||||
### Traefik IngressRoute support
|
### Traefik IngressRoute support
|
||||||
|
|
||||||
Homepage can also read ingresses defined using the Traefik IngressRoute custom resource definition. Due to the complex nature of Traefik routing rules, it is required for the `gethomepage.dev/href` annotation to be set:
|
Homepage can also read ingresses defined using the Traefik IngressRoute custom resource definition. Due to the complex nature of Traefik routing rules, it is required for the `gethomepage.dev/href` annotation to be set:
|
||||||
|
@ -71,7 +71,7 @@ Homepage provides a set of common translations that you can use in your widgets.
|
|||||||
| `common.ms` | `1,000 ms` | Format a number in milliseconds. |
|
| `common.ms` | `1,000 ms` | Format a number in milliseconds. |
|
||||||
| `common.date` | `2024-01-01` | Format a date. |
|
| `common.date` | `2024-01-01` | Format a date. |
|
||||||
| `common.relativeDate` | `1 day ago` | Format a relative date. |
|
| `common.relativeDate` | `1 day ago` | Format a relative date. |
|
||||||
| `common.uptime` | `1 day, 1 hour` | Format an uptime. |
|
| `common.duration` | `1 day, 1 hour` | Format an duration. |
|
||||||
|
|
||||||
### Text
|
### Text
|
||||||
|
|
||||||
|
@ -5,7 +5,11 @@ description: Unifi Controller Information Widget Configuration
|
|||||||
|
|
||||||
_(Find the Unifi Controller service widget [here](../services/unifi-controller.md))_
|
_(Find the Unifi Controller service widget [here](../services/unifi-controller.md))_
|
||||||
|
|
||||||
You can display general connectivity status from your Unifi (Network) Controller. When authenticating you will want to use a local account that has at least read privileges.
|
You can display general connectivity status from your Unifi (Network) Controller.
|
||||||
|
|
||||||
|
!!!
|
||||||
|
|
||||||
|
When authenticating you will want to use a local account that has at least read privileges.
|
||||||
|
|
||||||
An optional 'site' parameter can be supplied, if it is not the widget will use the default site for the controller.
|
An optional 'site' parameter can be supplied, if it is not the widget will use the default site for the controller.
|
||||||
|
|
||||||
|
33
docs/widgets/services/argocd.md
Normal file
33
docs/widgets/services/argocd.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
title: ArgoCD
|
||||||
|
description: ArgoCD Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [ArgoCD](https://argo-cd.readthedocs.io/en/stable/).
|
||||||
|
|
||||||
|
Allowed fields (limited to a max of 4): `["apps", "synced", "outOfSync", "healthy", "progressing", "degraded", "suspended", "missing"]`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: argocd
|
||||||
|
url: http://argocd.host.or.ip:port
|
||||||
|
key: argocdapikey
|
||||||
|
```
|
||||||
|
|
||||||
|
You can generate an API key either by creating a bearer token for an existing account, see [Authorization](https://argo-cd.readthedocs.io/en/latest/developer-guide/api-docs/#authorization) (not recommended) or create a new local user account with limited privileges and generate an authentication token for this account. To do this the steps are:
|
||||||
|
|
||||||
|
- [Create a new local user](https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/#create-new-user) and give it the `apiKey` capability
|
||||||
|
- Setup [RBAC configuration](https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#rbac-configuration) for your the user and give it readonly access to your ArgoCD resources, e.g. by giving it the `role:readonly` role.
|
||||||
|
- In your ArgoCD project under _Settings / Accounts_ open the newly created account and in the _Tokens_ section click on _Generate New_ to generate an access token, optionally specifying an expiry date.
|
||||||
|
|
||||||
|
If you installed ArgoCD via the official Helm chart, the account creation and rbac config can be achived by overriding these helm values:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
configs:
|
||||||
|
cm:
|
||||||
|
accounts.readonly: apiKey
|
||||||
|
rbac:
|
||||||
|
policy.csv: "g, readonly, role:readonly"
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a new account called `readonly` and attaches the `role:readonly` role to it.
|
22
docs/widgets/services/beszel.md
Normal file
22
docs/widgets/services/beszel.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
title: Beszel
|
||||||
|
description: Beszel Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Beszel](https://github.com/henrygd/beszel)
|
||||||
|
|
||||||
|
The widget has two modes, a single system with detailed info if `systemId` is provided, or an overview of all systems if `systemId` is not provided.
|
||||||
|
|
||||||
|
The `systemID` in the `id` field on the collections page of Beszel.
|
||||||
|
|
||||||
|
Allowed fields for 'overview' mode: `["systems", "up"]`
|
||||||
|
Allowed fields for a single system: `["name", "status", "updated", "cpu", "memory", "disk", "network"]`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: beszel
|
||||||
|
url: http://beszel.host.or.ip
|
||||||
|
username: username # email
|
||||||
|
password: password
|
||||||
|
systemId: systemId # optional
|
||||||
|
```
|
20
docs/widgets/services/gitlab.md
Normal file
20
docs/widgets/services/gitlab.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: Gitlab
|
||||||
|
description: Gitlab Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Gitlab](https://gitlab.com).
|
||||||
|
|
||||||
|
API requires a personal access token with either `read_api` or `api` permission. See the [gitlab documentation](https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#create-a-personal-access-token) for details on generating one.
|
||||||
|
|
||||||
|
Your Gitlab user ID can be found on [your profile page](https://support.circleci.com/hc/en-us/articles/20761157174043-How-to-find-your-GitLab-User-ID).
|
||||||
|
|
||||||
|
Allowed fields: `["events", "issues", "merges", "projects"]`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: gitlab
|
||||||
|
url: http://gitlab.host.or.ip:port
|
||||||
|
key: personal-access-token
|
||||||
|
user_id: 123456
|
||||||
|
```
|
19
docs/widgets/services/headscale.md
Normal file
19
docs/widgets/services/headscale.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
title: Headscale
|
||||||
|
description: Headscale Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Headscale](https://headscale.net/).
|
||||||
|
|
||||||
|
You will need to generate an API access token from the [command line](https://headscale.net/ref/remote-cli/#create-an-api-key) using `headscale apikeys create` command.
|
||||||
|
|
||||||
|
To find your node ID, you can use `headscale nodes list` command.
|
||||||
|
|
||||||
|
Allowed fields: `["name", "address", "last_seen", "status"]`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: headscale
|
||||||
|
nodeId: nodeid
|
||||||
|
key: headscaleapiaccesstoken
|
||||||
|
```
|
@ -8,12 +8,14 @@ search:
|
|||||||
You can also find a list of all available service widgets in the sidebar navigation.
|
You can also find a list of all available service widgets in the sidebar navigation.
|
||||||
|
|
||||||
- [Adguard Home](adguard-home.md)
|
- [Adguard Home](adguard-home.md)
|
||||||
|
- [ArgoCD](argocd.md)
|
||||||
- [Atsumeru](atsumeru.md)
|
- [Atsumeru](atsumeru.md)
|
||||||
- [Audiobookshelf](audiobookshelf.md)
|
- [Audiobookshelf](audiobookshelf.md)
|
||||||
- [Authentik](authentik.md)
|
- [Authentik](authentik.md)
|
||||||
- [Autobrr](autobrr.md)
|
- [Autobrr](autobrr.md)
|
||||||
- [Azure DevOps](azuredevops.md)
|
- [Azure DevOps](azuredevops.md)
|
||||||
- [Bazarr](bazarr.md)
|
- [Bazarr](bazarr.md)
|
||||||
|
- [Beszel](beszel.md)
|
||||||
- [Caddy](caddy.md)
|
- [Caddy](caddy.md)
|
||||||
- [Calendar](calendar.md)
|
- [Calendar](calendar.md)
|
||||||
- [Calibre-Web](calibre-web.md)
|
- [Calibre-Web](calibre-web.md)
|
||||||
@ -39,11 +41,13 @@ You can also find a list of all available service widgets in the sidebar navigat
|
|||||||
- [Gatus](gatus.md)
|
- [Gatus](gatus.md)
|
||||||
- [Ghostfolio](ghostfolio.md)
|
- [Ghostfolio](ghostfolio.md)
|
||||||
- [Gitea](gitea.md)
|
- [Gitea](gitea.md)
|
||||||
|
- [Gitlab](gitlab.md)
|
||||||
- [Glances](glances.md)
|
- [Glances](glances.md)
|
||||||
- [Gluetun](gluetun.md)
|
- [Gluetun](gluetun.md)
|
||||||
- [Gotify](gotify.md)
|
- [Gotify](gotify.md)
|
||||||
- [Grafana](grafana.md)
|
- [Grafana](grafana.md)
|
||||||
- [HDHomeRun](hdhomerun.md)
|
- [HDHomeRun](hdhomerun.md)
|
||||||
|
- [Headscale](headscale.md)
|
||||||
- [Healthchecks](healthchecks.md)
|
- [Healthchecks](healthchecks.md)
|
||||||
- [Home Assistant](homeassistant.md)
|
- [Home Assistant](homeassistant.md)
|
||||||
- [HomeBox](homebox.md)
|
- [HomeBox](homebox.md)
|
||||||
@ -96,6 +100,7 @@ You can also find a list of all available service widgets in the sidebar navigat
|
|||||||
- [Plex](plex.md)
|
- [Plex](plex.md)
|
||||||
- [Portainer](portainer.md)
|
- [Portainer](portainer.md)
|
||||||
- [Prometheus](prometheus.md)
|
- [Prometheus](prometheus.md)
|
||||||
|
- [Prometheus Metric](prometheusmetric.md)
|
||||||
- [Prowlarr](prowlarr.md)
|
- [Prowlarr](prowlarr.md)
|
||||||
- [Proxmox](proxmox.md)
|
- [Proxmox](proxmox.md)
|
||||||
- [Proxmox Backup Server](proxmoxbackupserver.md)
|
- [Proxmox Backup Server](proxmoxbackupserver.md)
|
||||||
|
67
docs/widgets/services/prometheusmetric.md
Normal file
67
docs/widgets/services/prometheusmetric.md
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
title: Prometheus Metric
|
||||||
|
description: Prometheus Metric Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Querying Prometheus](https://prometheus.io/docs/prometheus/latest/querying/basics/).
|
||||||
|
|
||||||
|
This widget can show metrics for your service defined by PromQL queries which are requested from a running Prometheus instance.
|
||||||
|
|
||||||
|
Quries can be defined in the `metrics` array of the widget along with a label to be used to present the metric value. You can optionally specify a global `refreshInterval` in milliseconds and/or define the `refreshInterval` per metric. Inside the optional `format` object of a metric various formatting styles and transformations can be applied (see below).
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: prometheusmetric
|
||||||
|
url: https://prometheus.host.or.ip
|
||||||
|
refreshInterval: 10000 # optional - in milliseconds, defaults to 10s
|
||||||
|
metrics:
|
||||||
|
- label: Metric 1
|
||||||
|
query: alertmanager_alerts{state="active"}
|
||||||
|
- label: Metric 2
|
||||||
|
query: apiserver_storage_size_bytes{node="mynode"}
|
||||||
|
format:
|
||||||
|
type: bytes
|
||||||
|
- label: Metric 3
|
||||||
|
query: avg(prometheus_notifications_latency_seconds)
|
||||||
|
format:
|
||||||
|
type: number
|
||||||
|
suffix: s
|
||||||
|
options:
|
||||||
|
maximumFractionDigits: 4
|
||||||
|
- label: Metric 4
|
||||||
|
query: time()
|
||||||
|
refreshInterval: 1000 # will override global refreshInterval
|
||||||
|
format:
|
||||||
|
type: date
|
||||||
|
scale: 1000
|
||||||
|
options:
|
||||||
|
timeStyle: medium
|
||||||
|
```
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
Supported values for `format.type` are `text`, `number`, `percent`, `bytes`, `bits`, `bbytes`, `bbits`, `byterate`, `bibyterate`, `bitrate`, `bibitrate`, `date`, `duration`, `relativeDate`, and `text` which is the default.
|
||||||
|
|
||||||
|
The `dateStyle` and `timeStyle` options of the `date` format are passed directly to [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat) and the `style` and `numeric` options of `relativeDate` are passed to [Intl.RelativeTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat). For the `number` format, options of [Intl.NumberFormat](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat) can be used, e.g. `maximumFractionDigits` or `minimumFractionDigits`.
|
||||||
|
|
||||||
|
### Data Transformation
|
||||||
|
|
||||||
|
You can manipulate your metric value with the following tools: `scale`, `prefix` and `suffix`, for example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- query: my_custom_metric{}
|
||||||
|
label: Metric 1
|
||||||
|
format:
|
||||||
|
type: number
|
||||||
|
scale: 1000 # multiplies value by a number or fraction string e.g. 1/16
|
||||||
|
- query: my_custom_metric{}
|
||||||
|
label: Metric 2
|
||||||
|
format:
|
||||||
|
type: number
|
||||||
|
prefix: "$" # prefixes value with given string
|
||||||
|
- query: my_custom_metric{}
|
||||||
|
label: Metric 3
|
||||||
|
format:
|
||||||
|
type: number
|
||||||
|
suffix: "€" # suffixes value with given string
|
||||||
|
```
|
15
docs/widgets/services/spoolman.md
Normal file
15
docs/widgets/services/spoolman.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: Spoolman
|
||||||
|
description: Spoolman Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Spoolman](https://github.com/Donkie/Spoolman).
|
||||||
|
|
||||||
|
4 spools are displayed by default. If more than 4 spools are configured in spoolman you can use the spoolIds configuration option to control which are displayed.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: spoolman
|
||||||
|
url: http://spoolman.host.or.ip
|
||||||
|
spoolIds: [1, 2, 3, 4] # optional
|
||||||
|
```
|
20
docs/widgets/services/suwayomi.md
Normal file
20
docs/widgets/services/suwayomi.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
title: Suwayomi
|
||||||
|
description: Suwayomi Widget Configuration
|
||||||
|
---
|
||||||
|
|
||||||
|
Learn more about [Suwayomi](https://github.com/Suwayomi/Suwayomi-Server).
|
||||||
|
|
||||||
|
Allowed fields: ["download", "nondownload", "read", "unread", "downloadedread", "downloadedunread", "nondownloadedread", "nondownloadedunread"]
|
||||||
|
|
||||||
|
The widget defaults to the first four above. If more than four fields are provided, only the first 4 are displayed.
|
||||||
|
Category IDs can be obtained from the url when navigating to it, `?tab={categoryID}`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
widget:
|
||||||
|
type: suwayomi
|
||||||
|
url: http://suwayomi.host.or.ip
|
||||||
|
username: username #optional
|
||||||
|
password: password #optional
|
||||||
|
category: 0 #optional, defaults to all categories
|
||||||
|
```
|
@ -7,7 +7,11 @@ Learn more about [Unifi Controller](https://ui.com/).
|
|||||||
|
|
||||||
_(Find the Unifi Controller information widget [here](../info/unifi_controller.md))_
|
_(Find the Unifi Controller information widget [here](../info/unifi_controller.md))_
|
||||||
|
|
||||||
You can display general connectivity status from your Unifi (Network) Controller. When authenticating you will want to use a local account that has at least read privileges.
|
You can display general connectivity status from your Unifi (Network) Controller.
|
||||||
|
|
||||||
|
!!!
|
||||||
|
|
||||||
|
When authenticating you will want to use a local account that has at least read privileges.
|
||||||
|
|
||||||
An optional 'site' parameter can be supplied, if it is not the widget will use the default site for the controller.
|
An optional 'site' parameter can be supplied, if it is not the widget will use the default site for the controller.
|
||||||
|
|
||||||
|
@ -31,12 +31,14 @@ nav:
|
|||||||
- "Service Widgets":
|
- "Service Widgets":
|
||||||
- widgets/services/index.md
|
- widgets/services/index.md
|
||||||
- widgets/services/adguard-home.md
|
- widgets/services/adguard-home.md
|
||||||
|
- widgets/services/argocd.md
|
||||||
- widgets/services/atsumeru.md
|
- widgets/services/atsumeru.md
|
||||||
- widgets/services/audiobookshelf.md
|
- widgets/services/audiobookshelf.md
|
||||||
- widgets/services/authentik.md
|
- widgets/services/authentik.md
|
||||||
- widgets/services/autobrr.md
|
- widgets/services/autobrr.md
|
||||||
- widgets/services/azuredevops.md
|
- widgets/services/azuredevops.md
|
||||||
- widgets/services/bazarr.md
|
- widgets/services/bazarr.md
|
||||||
|
- widgets/services/beszel.md
|
||||||
- widgets/services/caddy.md
|
- widgets/services/caddy.md
|
||||||
- widgets/services/calendar.md
|
- widgets/services/calendar.md
|
||||||
- widgets/services/calibre-web.md
|
- widgets/services/calibre-web.md
|
||||||
@ -62,11 +64,13 @@ nav:
|
|||||||
- widgets/services/gatus.md
|
- widgets/services/gatus.md
|
||||||
- widgets/services/ghostfolio.md
|
- widgets/services/ghostfolio.md
|
||||||
- widgets/services/gitea.md
|
- widgets/services/gitea.md
|
||||||
|
- widgets/services/gitlab.md
|
||||||
- widgets/services/glances.md
|
- widgets/services/glances.md
|
||||||
- widgets/services/gluetun.md
|
- widgets/services/gluetun.md
|
||||||
- widgets/services/gotify.md
|
- widgets/services/gotify.md
|
||||||
- widgets/services/grafana.md
|
- widgets/services/grafana.md
|
||||||
- widgets/services/hdhomerun.md
|
- widgets/services/hdhomerun.md
|
||||||
|
- widgets/services/headscale.md
|
||||||
- widgets/services/healthchecks.md
|
- widgets/services/healthchecks.md
|
||||||
- widgets/services/homeassistant.md
|
- widgets/services/homeassistant.md
|
||||||
- widgets/services/homebox.md
|
- widgets/services/homebox.md
|
||||||
@ -119,6 +123,7 @@ nav:
|
|||||||
- widgets/services/plex.md
|
- widgets/services/plex.md
|
||||||
- widgets/services/portainer.md
|
- widgets/services/portainer.md
|
||||||
- widgets/services/prometheus.md
|
- widgets/services/prometheus.md
|
||||||
|
- widgets/services/prometheusmetric.md
|
||||||
- widgets/services/prowlarr.md
|
- widgets/services/prowlarr.md
|
||||||
- widgets/services/proxmox.md
|
- widgets/services/proxmox.md
|
||||||
- widgets/services/proxmoxbackupserver.md
|
- widgets/services/proxmoxbackupserver.md
|
||||||
@ -134,6 +139,7 @@ nav:
|
|||||||
- widgets/services/scrutiny.md
|
- widgets/services/scrutiny.md
|
||||||
- widgets/services/sonarr.md
|
- widgets/services/sonarr.md
|
||||||
- widgets/services/speedtest-tracker.md
|
- widgets/services/speedtest-tracker.md
|
||||||
|
- widgets/services/spoolman.md
|
||||||
- widgets/services/stash.md
|
- widgets/services/stash.md
|
||||||
- widgets/services/stocks.md
|
- widgets/services/stocks.md
|
||||||
- widgets/services/swagdashboard.md
|
- widgets/services/swagdashboard.md
|
||||||
|
@ -84,12 +84,12 @@ function prettyBytes(number, options) {
|
|||||||
return `${prefix + numberString} ${unit}`;
|
return `${prefix + numberString} ${unit}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function uptime(uptimeInSeconds, i18next) {
|
function duration(durationInSeconds, i18next) {
|
||||||
const mo = Math.floor(uptimeInSeconds / (3600 * 24 * 31));
|
const mo = Math.floor(durationInSeconds / (3600 * 24 * 31));
|
||||||
const d = Math.floor((uptimeInSeconds % (3600 * 24 * 31)) / (3600 * 24));
|
const d = Math.floor((durationInSeconds % (3600 * 24 * 31)) / (3600 * 24));
|
||||||
const h = Math.floor((uptimeInSeconds % (3600 * 24)) / 3600);
|
const h = Math.floor((durationInSeconds % (3600 * 24)) / 3600);
|
||||||
const m = Math.floor((uptimeInSeconds % 3600) / 60);
|
const m = Math.floor((durationInSeconds % 3600) / 60);
|
||||||
const s = Math.floor(uptimeInSeconds % 60);
|
const s = Math.floor(durationInSeconds % 60);
|
||||||
|
|
||||||
const moDisplay = mo > 0 ? mo + i18next.t("common.months") : "";
|
const moDisplay = mo > 0 ? mo + i18next.t("common.months") : "";
|
||||||
const dDisplay = d > 0 ? d + i18next.t("common.days") : "";
|
const dDisplay = d > 0 ? d + i18next.t("common.days") : "";
|
||||||
@ -156,7 +156,7 @@ module.exports = {
|
|||||||
i18next.services.formatter.add("relativeDate", (value, lng, options) =>
|
i18next.services.formatter.add("relativeDate", (value, lng, options) =>
|
||||||
relativeDate(new Date(value), new Intl.RelativeTimeFormat(lng, { ...options })),
|
relativeDate(new Date(value), new Intl.RelativeTimeFormat(lng, { ...options })),
|
||||||
);
|
);
|
||||||
i18next.services.formatter.add("uptime", (value, lng) => uptime(value, i18next));
|
i18next.services.formatter.add("duration", (value, lng) => duration(value, i18next));
|
||||||
},
|
},
|
||||||
type: "3rdParty",
|
type: "3rdParty",
|
||||||
},
|
},
|
||||||
|
46
package-lock.json
generated
46
package-lock.json
generated
@ -36,7 +36,7 @@
|
|||||||
"swr": "^1.3.0",
|
"swr": "^1.3.0",
|
||||||
"systeminformation": "^5.23.2",
|
"systeminformation": "^5.23.2",
|
||||||
"tough-cookie": "^4.1.3",
|
"tough-cookie": "^4.1.3",
|
||||||
"urbackup-server-api": "^0.52.0",
|
"urbackup-server-api": "^0.52.1",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"xml-js": "^1.6.11"
|
"xml-js": "^1.6.11"
|
||||||
},
|
},
|
||||||
@ -47,7 +47,7 @@
|
|||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-next": "^14.2.3",
|
"eslint-config-next": "^14.2.3",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-react": "^7.37.1",
|
"eslint-plugin-react": "^7.37.1",
|
||||||
@ -55,8 +55,8 @@
|
|||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"tailwind-scrollbar": "^3.0.5",
|
"tailwind-scrollbar": "^3.0.5",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.14",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"osx-temperature-sensor": "^1.0.8"
|
"osx-temperature-sensor": "^1.0.8"
|
||||||
@ -3135,11 +3135,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-module-utils": {
|
"node_modules/eslint-module-utils": {
|
||||||
"version": "2.9.0",
|
"version": "2.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
|
||||||
"integrity": "sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==",
|
"integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^3.2.7"
|
"debug": "^3.2.7"
|
||||||
},
|
},
|
||||||
@ -3163,11 +3162,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-import": {
|
"node_modules/eslint-plugin-import": {
|
||||||
"version": "2.30.0",
|
"version": "2.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
|
||||||
"integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==",
|
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rtsao/scc": "^1.1.0",
|
"@rtsao/scc": "^1.1.0",
|
||||||
"array-includes": "^3.1.8",
|
"array-includes": "^3.1.8",
|
||||||
@ -3177,7 +3175,7 @@
|
|||||||
"debug": "^3.2.7",
|
"debug": "^3.2.7",
|
||||||
"doctrine": "^2.1.0",
|
"doctrine": "^2.1.0",
|
||||||
"eslint-import-resolver-node": "^0.3.9",
|
"eslint-import-resolver-node": "^0.3.9",
|
||||||
"eslint-module-utils": "^2.9.0",
|
"eslint-module-utils": "^2.12.0",
|
||||||
"hasown": "^2.0.2",
|
"hasown": "^2.0.2",
|
||||||
"is-core-module": "^2.15.1",
|
"is-core-module": "^2.15.1",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
@ -3186,13 +3184,14 @@
|
|||||||
"object.groupby": "^1.0.3",
|
"object.groupby": "^1.0.3",
|
||||||
"object.values": "^1.2.0",
|
"object.values": "^1.2.0",
|
||||||
"semver": "^6.3.1",
|
"semver": "^6.3.1",
|
||||||
|
"string.prototype.trimend": "^1.0.8",
|
||||||
"tsconfig-paths": "^3.15.0"
|
"tsconfig-paths": "^3.15.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
|
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-import/node_modules/debug": {
|
"node_modules/eslint-plugin-import/node_modules/debug": {
|
||||||
@ -7713,9 +7712,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "3.4.13",
|
"version": "3.4.14",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz",
|
||||||
"integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
|
"integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alloc/quick-lru": "^5.2.0",
|
"@alloc/quick-lru": "^5.2.0",
|
||||||
@ -8102,9 +8101,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.6.2",
|
"version": "5.6.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@ -8186,10 +8185,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/urbackup-server-api": {
|
"node_modules/urbackup-server-api": {
|
||||||
"version": "0.52.0",
|
"version": "0.52.1",
|
||||||
"resolved": "https://registry.npmjs.org/urbackup-server-api/-/urbackup-server-api-0.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/urbackup-server-api/-/urbackup-server-api-0.52.1.tgz",
|
||||||
"integrity": "sha512-KfroCFZEWCuCkWye1F1JwI2fkO1za/Mf1a8TNGTujzxU0ZGzDqhA1zCOcvV97q7nH1TKFNpw5tMZ06fSCKv2UA==",
|
"integrity": "sha512-gAxF9MdXxnceqUr/1Uj2LuGZQb/bvZ3Ply9zH/UTSWGkwKL5C0qMPrBvKRyTHbPMG/NBuHF6BzavkF7GNvOLew==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"node-fetch": "^2.7.0"
|
"node-fetch": "^2.7.0"
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
"swr": "^1.3.0",
|
"swr": "^1.3.0",
|
||||||
"systeminformation": "^5.23.2",
|
"systeminformation": "^5.23.2",
|
||||||
"tough-cookie": "^4.1.3",
|
"tough-cookie": "^4.1.3",
|
||||||
"urbackup-server-api": "^0.52.0",
|
"urbackup-server-api": "^0.52.1",
|
||||||
"winston": "^3.11.0",
|
"winston": "^3.11.0",
|
||||||
"xml-js": "^1.6.11"
|
"xml-js": "^1.6.11"
|
||||||
},
|
},
|
||||||
@ -49,7 +49,7 @@
|
|||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-next": "^14.2.3",
|
"eslint-config-next": "^14.2.3",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-import": "^2.29.1",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-react": "^7.37.1",
|
"eslint-plugin-react": "^7.37.1",
|
||||||
@ -57,8 +57,8 @@
|
|||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"tailwind-scrollbar": "^3.0.5",
|
"tailwind-scrollbar": "^3.0.5",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.14",
|
||||||
"typescript": "^5.6.2"
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"osx-temperature-sensor": "^1.0.8"
|
"osx-temperature-sensor": "^1.0.8"
|
||||||
|
172
pnpm-lock.yaml
generated
172
pnpm-lock.yaml
generated
@ -93,8 +93,8 @@ importers:
|
|||||||
specifier: ^4.1.3
|
specifier: ^4.1.3
|
||||||
version: 4.1.4
|
version: 4.1.4
|
||||||
urbackup-server-api:
|
urbackup-server-api:
|
||||||
specifier: ^0.52.0
|
specifier: ^0.52.1
|
||||||
version: 0.52.0
|
version: 0.52.1
|
||||||
winston:
|
winston:
|
||||||
specifier: ^3.11.0
|
specifier: ^3.11.0
|
||||||
version: 3.14.2
|
version: 3.14.2
|
||||||
@ -108,7 +108,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@tailwindcss/forms':
|
'@tailwindcss/forms':
|
||||||
specifier: ^0.5.8
|
specifier: ^0.5.8
|
||||||
version: 0.5.9(tailwindcss@3.4.13)
|
version: 0.5.9(tailwindcss@3.4.14)
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.20
|
specifier: ^10.4.20
|
||||||
version: 10.4.20(postcss@8.4.47)
|
version: 10.4.20(postcss@8.4.47)
|
||||||
@ -117,16 +117,16 @@ importers:
|
|||||||
version: 8.57.1
|
version: 8.57.1
|
||||||
eslint-config-airbnb:
|
eslint-config-airbnb:
|
||||||
specifier: ^19.0.4
|
specifier: ^19.0.4
|
||||||
version: 19.0.4(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.0(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.1(eslint@8.57.1))(eslint@8.57.1)
|
version: 19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.0(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.1(eslint@8.57.1))(eslint@8.57.1)
|
||||||
eslint-config-next:
|
eslint-config-next:
|
||||||
specifier: ^14.2.3
|
specifier: ^14.2.3
|
||||||
version: 14.2.8(eslint@8.57.1)(typescript@5.6.2)
|
version: 14.2.8(eslint@8.57.1)(typescript@5.6.3)
|
||||||
eslint-config-prettier:
|
eslint-config-prettier:
|
||||||
specifier: ^9.1.0
|
specifier: ^9.1.0
|
||||||
version: 9.1.0(eslint@8.57.1)
|
version: 9.1.0(eslint@8.57.1)
|
||||||
eslint-plugin-import:
|
eslint-plugin-import:
|
||||||
specifier: ^2.29.1
|
specifier: ^2.31.0
|
||||||
version: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
version: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
||||||
eslint-plugin-jsx-a11y:
|
eslint-plugin-jsx-a11y:
|
||||||
specifier: ^6.8.0
|
specifier: ^6.8.0
|
||||||
version: 6.10.0(eslint@8.57.1)
|
version: 6.10.0(eslint@8.57.1)
|
||||||
@ -147,13 +147,13 @@ importers:
|
|||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
tailwind-scrollbar:
|
tailwind-scrollbar:
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.1.0(tailwindcss@3.4.13)
|
version: 3.1.0(tailwindcss@3.4.14)
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^3.4.13
|
specifier: ^3.4.14
|
||||||
version: 3.4.13
|
version: 3.4.14
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.6.2
|
specifier: ^5.6.3
|
||||||
version: 5.6.2
|
version: 5.6.3
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -1077,6 +1077,27 @@ packages:
|
|||||||
eslint-plugin-import-x:
|
eslint-plugin-import-x:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
eslint-module-utils@2.12.0:
|
||||||
|
resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
peerDependencies:
|
||||||
|
'@typescript-eslint/parser': '*'
|
||||||
|
eslint: '*'
|
||||||
|
eslint-import-resolver-node: '*'
|
||||||
|
eslint-import-resolver-typescript: '*'
|
||||||
|
eslint-import-resolver-webpack: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@typescript-eslint/parser':
|
||||||
|
optional: true
|
||||||
|
eslint:
|
||||||
|
optional: true
|
||||||
|
eslint-import-resolver-node:
|
||||||
|
optional: true
|
||||||
|
eslint-import-resolver-typescript:
|
||||||
|
optional: true
|
||||||
|
eslint-import-resolver-webpack:
|
||||||
|
optional: true
|
||||||
|
|
||||||
eslint-module-utils@2.9.0:
|
eslint-module-utils@2.9.0:
|
||||||
resolution: {integrity: sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==}
|
resolution: {integrity: sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -1098,12 +1119,12 @@ packages:
|
|||||||
eslint-import-resolver-webpack:
|
eslint-import-resolver-webpack:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
eslint-plugin-import@2.30.0:
|
eslint-plugin-import@2.31.0:
|
||||||
resolution: {integrity: sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==}
|
resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@typescript-eslint/parser': '*'
|
'@typescript-eslint/parser': '*'
|
||||||
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
|
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
'@typescript-eslint/parser':
|
'@typescript-eslint/parser':
|
||||||
optional: true
|
optional: true
|
||||||
@ -1151,6 +1172,7 @@ packages:
|
|||||||
eslint@8.57.1:
|
eslint@8.57.1:
|
||||||
resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
|
resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
espree@9.6.1:
|
espree@9.6.1:
|
||||||
@ -2510,8 +2532,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
tailwindcss: 3.x
|
tailwindcss: 3.x
|
||||||
|
|
||||||
tailwindcss@3.4.13:
|
tailwindcss@3.4.14:
|
||||||
resolution: {integrity: sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==}
|
resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -2624,8 +2646,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==}
|
resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
typescript@5.6.2:
|
typescript@5.6.3:
|
||||||
resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==}
|
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@ -2649,8 +2671,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
browserslist: '>= 4.21.0'
|
browserslist: '>= 4.21.0'
|
||||||
|
|
||||||
urbackup-server-api@0.52.0:
|
urbackup-server-api@0.52.1:
|
||||||
resolution: {integrity: sha512-KfroCFZEWCuCkWye1F1JwI2fkO1za/Mf1a8TNGTujzxU0ZGzDqhA1zCOcvV97q7nH1TKFNpw5tMZ06fSCKv2UA==}
|
resolution: {integrity: sha512-gAxF9MdXxnceqUr/1Uj2LuGZQb/bvZ3Ply9zH/UTSWGkwKL5C0qMPrBvKRyTHbPMG/NBuHF6BzavkF7GNvOLew==}
|
||||||
|
|
||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
@ -2956,10 +2978,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
defer-to-connect: 2.0.1
|
defer-to-connect: 2.0.1
|
||||||
|
|
||||||
'@tailwindcss/forms@0.5.9(tailwindcss@3.4.13)':
|
'@tailwindcss/forms@0.5.9(tailwindcss@3.4.14)':
|
||||||
dependencies:
|
dependencies:
|
||||||
mini-svg-data-uri: 1.4.4
|
mini-svg-data-uri: 1.4.4
|
||||||
tailwindcss: 3.4.13
|
tailwindcss: 3.4.14
|
||||||
|
|
||||||
'@tanstack/react-virtual@3.10.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
'@tanstack/react-virtual@3.10.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -3015,13 +3037,13 @@ snapshots:
|
|||||||
|
|
||||||
'@types/triple-beam@1.3.5': {}
|
'@types/triple-beam@1.3.5': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)':
|
'@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.11.0
|
'@eslint-community/regexpp': 4.11.0
|
||||||
'@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.6.2)
|
'@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.6.3)
|
||||||
'@typescript-eslint/scope-manager': 7.2.0
|
'@typescript-eslint/scope-manager': 7.2.0
|
||||||
'@typescript-eslint/type-utils': 7.2.0(eslint@8.57.1)(typescript@5.6.2)
|
'@typescript-eslint/type-utils': 7.2.0(eslint@8.57.1)(typescript@5.6.3)
|
||||||
'@typescript-eslint/utils': 7.2.0(eslint@8.57.1)(typescript@5.6.2)
|
'@typescript-eslint/utils': 7.2.0(eslint@8.57.1)(typescript@5.6.3)
|
||||||
'@typescript-eslint/visitor-keys': 7.2.0
|
'@typescript-eslint/visitor-keys': 7.2.0
|
||||||
debug: 4.3.6
|
debug: 4.3.6
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
@ -3029,22 +3051,22 @@ snapshots:
|
|||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
semver: 7.6.3
|
semver: 7.6.3
|
||||||
ts-api-utils: 1.3.0(typescript@5.6.2)
|
ts-api-utils: 1.3.0(typescript@5.6.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.6.2
|
typescript: 5.6.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2)':
|
'@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/scope-manager': 7.2.0
|
'@typescript-eslint/scope-manager': 7.2.0
|
||||||
'@typescript-eslint/types': 7.2.0
|
'@typescript-eslint/types': 7.2.0
|
||||||
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.6.2)
|
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.6.3)
|
||||||
'@typescript-eslint/visitor-keys': 7.2.0
|
'@typescript-eslint/visitor-keys': 7.2.0
|
||||||
debug: 4.3.6
|
debug: 4.3.6
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.6.2
|
typescript: 5.6.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@ -3053,21 +3075,21 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 7.2.0
|
'@typescript-eslint/types': 7.2.0
|
||||||
'@typescript-eslint/visitor-keys': 7.2.0
|
'@typescript-eslint/visitor-keys': 7.2.0
|
||||||
|
|
||||||
'@typescript-eslint/type-utils@7.2.0(eslint@8.57.1)(typescript@5.6.2)':
|
'@typescript-eslint/type-utils@7.2.0(eslint@8.57.1)(typescript@5.6.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.6.2)
|
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.6.3)
|
||||||
'@typescript-eslint/utils': 7.2.0(eslint@8.57.1)(typescript@5.6.2)
|
'@typescript-eslint/utils': 7.2.0(eslint@8.57.1)(typescript@5.6.3)
|
||||||
debug: 4.3.6
|
debug: 4.3.6
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
ts-api-utils: 1.3.0(typescript@5.6.2)
|
ts-api-utils: 1.3.0(typescript@5.6.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.6.2
|
typescript: 5.6.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/types@7.2.0': {}
|
'@typescript-eslint/types@7.2.0': {}
|
||||||
|
|
||||||
'@typescript-eslint/typescript-estree@7.2.0(typescript@5.6.2)':
|
'@typescript-eslint/typescript-estree@7.2.0(typescript@5.6.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/types': 7.2.0
|
'@typescript-eslint/types': 7.2.0
|
||||||
'@typescript-eslint/visitor-keys': 7.2.0
|
'@typescript-eslint/visitor-keys': 7.2.0
|
||||||
@ -3076,20 +3098,20 @@ snapshots:
|
|||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.3
|
minimatch: 9.0.3
|
||||||
semver: 7.6.3
|
semver: 7.6.3
|
||||||
ts-api-utils: 1.3.0(typescript@5.6.2)
|
ts-api-utils: 1.3.0(typescript@5.6.3)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.6.2
|
typescript: 5.6.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@typescript-eslint/utils@7.2.0(eslint@8.57.1)(typescript@5.6.2)':
|
'@typescript-eslint/utils@7.2.0(eslint@8.57.1)(typescript@5.6.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
|
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
'@types/semver': 7.5.8
|
'@types/semver': 7.5.8
|
||||||
'@typescript-eslint/scope-manager': 7.2.0
|
'@typescript-eslint/scope-manager': 7.2.0
|
||||||
'@typescript-eslint/types': 7.2.0
|
'@typescript-eslint/types': 7.2.0
|
||||||
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.6.2)
|
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.6.3)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
semver: 7.6.3
|
semver: 7.6.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@ -3759,41 +3781,41 @@ snapshots:
|
|||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
|
|
||||||
eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1):
|
eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
confusing-browser-globals: 1.0.11
|
confusing-browser-globals: 1.0.11
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
||||||
object.assign: 4.1.5
|
object.assign: 4.1.5
|
||||||
object.entries: 1.1.8
|
object.entries: 1.1.8
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
|
|
||||||
eslint-config-airbnb@19.0.4(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.0(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.1(eslint@8.57.1))(eslint@8.57.1):
|
eslint-config-airbnb@19.0.4(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint-plugin-jsx-a11y@6.10.0(eslint@8.57.1))(eslint-plugin-react-hooks@4.6.2(eslint@8.57.1))(eslint-plugin-react@7.37.1(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1)
|
eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1))(eslint@8.57.1)
|
||||||
eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
||||||
eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1)
|
eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1)
|
||||||
eslint-plugin-react: 7.37.1(eslint@8.57.1)
|
eslint-plugin-react: 7.37.1(eslint@8.57.1)
|
||||||
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
|
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
|
||||||
object.assign: 4.1.5
|
object.assign: 4.1.5
|
||||||
object.entries: 1.1.8
|
object.entries: 1.1.8
|
||||||
|
|
||||||
eslint-config-next@14.2.8(eslint@8.57.1)(typescript@5.6.2):
|
eslint-config-next@14.2.8(eslint@8.57.1)(typescript@5.6.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/eslint-plugin-next': 14.2.8
|
'@next/eslint-plugin-next': 14.2.8
|
||||||
'@rushstack/eslint-patch': 1.10.4
|
'@rushstack/eslint-patch': 1.10.4
|
||||||
'@typescript-eslint/eslint-plugin': 7.2.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)
|
'@typescript-eslint/eslint-plugin': 7.2.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)
|
||||||
'@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.6.2)
|
'@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.6.3)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1)
|
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1)
|
||||||
eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
||||||
eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1)
|
eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1)
|
||||||
eslint-plugin-react: 7.37.1(eslint@8.57.1)
|
eslint-plugin-react: 7.37.1(eslint@8.57.1)
|
||||||
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
|
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.6.2
|
typescript: 5.6.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- eslint-import-resolver-webpack
|
- eslint-import-resolver-webpack
|
||||||
- eslint-plugin-import-x
|
- eslint-plugin-import-x
|
||||||
@ -3811,37 +3833,48 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1):
|
eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.3.6
|
debug: 4.3.6
|
||||||
enhanced-resolve: 5.17.1
|
enhanced-resolve: 5.17.1
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-module-utils: 2.9.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1)
|
eslint-module-utils: 2.9.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)
|
||||||
fast-glob: 3.3.2
|
fast-glob: 3.3.2
|
||||||
get-tsconfig: 4.8.0
|
get-tsconfig: 4.8.0
|
||||||
is-bun-module: 1.1.0
|
is-bun-module: 1.1.0
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@typescript-eslint/parser'
|
- '@typescript-eslint/parser'
|
||||||
- eslint-import-resolver-node
|
- eslint-import-resolver-node
|
||||||
- eslint-import-resolver-webpack
|
- eslint-import-resolver-webpack
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-module-utils@2.9.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1):
|
eslint-module-utils@2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7
|
debug: 3.2.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.6.2)
|
'@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.6.3)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1)
|
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1):
|
eslint-module-utils@2.9.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1):
|
||||||
|
dependencies:
|
||||||
|
debug: 3.2.7
|
||||||
|
optionalDependencies:
|
||||||
|
'@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.6.3)
|
||||||
|
eslint: 8.57.1
|
||||||
|
eslint-import-resolver-node: 0.3.9
|
||||||
|
eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.8
|
array-includes: 3.1.8
|
||||||
@ -3852,7 +3885,7 @@ snapshots:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-module-utils: 2.9.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.0)(eslint@8.57.1))(eslint@8.57.1)
|
eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.2.0(eslint@8.57.1)(typescript@5.6.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
is-core-module: 2.15.1
|
is-core-module: 2.15.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@ -3861,9 +3894,10 @@ snapshots:
|
|||||||
object.groupby: 1.0.3
|
object.groupby: 1.0.3
|
||||||
object.values: 1.2.0
|
object.values: 1.2.0
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
|
string.prototype.trimend: 1.0.8
|
||||||
tsconfig-paths: 3.15.0
|
tsconfig-paths: 3.15.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.6.2)
|
'@typescript-eslint/parser': 7.2.0(eslint@8.57.1)(typescript@5.6.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- eslint-import-resolver-typescript
|
- eslint-import-resolver-typescript
|
||||||
- eslint-import-resolver-webpack
|
- eslint-import-resolver-webpack
|
||||||
@ -5355,11 +5389,11 @@ snapshots:
|
|||||||
|
|
||||||
systeminformation@5.23.5: {}
|
systeminformation@5.23.5: {}
|
||||||
|
|
||||||
tailwind-scrollbar@3.1.0(tailwindcss@3.4.13):
|
tailwind-scrollbar@3.1.0(tailwindcss@3.4.14):
|
||||||
dependencies:
|
dependencies:
|
||||||
tailwindcss: 3.4.13
|
tailwindcss: 3.4.14
|
||||||
|
|
||||||
tailwindcss@3.4.13:
|
tailwindcss@3.4.14:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@alloc/quick-lru': 5.2.0
|
'@alloc/quick-lru': 5.2.0
|
||||||
arg: 5.0.2
|
arg: 5.0.2
|
||||||
@ -5454,9 +5488,9 @@ snapshots:
|
|||||||
|
|
||||||
triple-beam@1.4.1: {}
|
triple-beam@1.4.1: {}
|
||||||
|
|
||||||
ts-api-utils@1.3.0(typescript@5.6.2):
|
ts-api-utils@1.3.0(typescript@5.6.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.6.2
|
typescript: 5.6.3
|
||||||
|
|
||||||
ts-interface-checker@0.1.13: {}
|
ts-interface-checker@0.1.13: {}
|
||||||
|
|
||||||
@ -5515,7 +5549,7 @@ snapshots:
|
|||||||
is-typed-array: 1.1.13
|
is-typed-array: 1.1.13
|
||||||
possible-typed-array-names: 1.0.0
|
possible-typed-array-names: 1.0.0
|
||||||
|
|
||||||
typescript@5.6.2: {}
|
typescript@5.6.3: {}
|
||||||
|
|
||||||
unbox-primitive@1.0.2:
|
unbox-primitive@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -5536,7 +5570,7 @@ snapshots:
|
|||||||
escalade: 3.2.0
|
escalade: 3.2.0
|
||||||
picocolors: 1.1.0
|
picocolors: 1.1.0
|
||||||
|
|
||||||
urbackup-server-api@0.52.0:
|
urbackup-server-api@0.52.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
async-mutex: 0.5.0
|
async-mutex: 0.5.0
|
||||||
node-fetch: 2.7.0
|
node-fetch: 2.7.0
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
"ms": "{{value, number}}",
|
"ms": "{{value, number}}",
|
||||||
"date": "{{value, date}}",
|
"date": "{{value, date}}",
|
||||||
"relativeDate": "{{value, relativeDate}}",
|
"relativeDate": "{{value, relativeDate}}",
|
||||||
"uptime": "{{value, uptime}}",
|
"duration": "{{value, duration}}",
|
||||||
"months": "mo",
|
"months": "mo",
|
||||||
"days": "d",
|
"days": "d",
|
||||||
"hours": "h",
|
"hours": "h",
|
||||||
@ -309,6 +309,16 @@
|
|||||||
"stopped": "Stopped",
|
"stopped": "Stopped",
|
||||||
"total": "Total"
|
"total": "Total"
|
||||||
},
|
},
|
||||||
|
"suwayomi": {
|
||||||
|
"download": "Downloaded",
|
||||||
|
"nondownload": "Non-Downloaded",
|
||||||
|
"read": "Read",
|
||||||
|
"unread": "Unread",
|
||||||
|
"downloadedread": "Downloaded & Read",
|
||||||
|
"downloadedunread": "Downloaded & Unread",
|
||||||
|
"nondownloadedread": "Non-Downloaded & Read",
|
||||||
|
"nondownloadedunread": "Non-Downloaded & Unread"
|
||||||
|
},
|
||||||
"tailscale": {
|
"tailscale": {
|
||||||
"address": "Address",
|
"address": "Address",
|
||||||
"expires": "Expires",
|
"expires": "Expires",
|
||||||
@ -959,5 +969,43 @@
|
|||||||
"tasks7d": "Tasks Due This Week",
|
"tasks7d": "Tasks Due This Week",
|
||||||
"tasksOverdue": "Overdue Tasks",
|
"tasksOverdue": "Overdue Tasks",
|
||||||
"tasksInProgress": "Tasks In Progress"
|
"tasksInProgress": "Tasks In Progress"
|
||||||
|
},
|
||||||
|
"headscale": {
|
||||||
|
"name": "Name",
|
||||||
|
"address": "Address",
|
||||||
|
"last_seen": "Last Seen",
|
||||||
|
"status": "Status",
|
||||||
|
"online": "Online",
|
||||||
|
"offline": "Offline"
|
||||||
|
},
|
||||||
|
"beszel": {
|
||||||
|
"name": "Name",
|
||||||
|
"systems": "Systems",
|
||||||
|
"up": "Up",
|
||||||
|
"status": "Status",
|
||||||
|
"updated": "Updated",
|
||||||
|
"cpu": "CPU",
|
||||||
|
"memory": "MEM",
|
||||||
|
"disk": "Disk",
|
||||||
|
"network": "NET"
|
||||||
|
},
|
||||||
|
"argocd": {
|
||||||
|
"apps": "Apps",
|
||||||
|
"synced": "Synced",
|
||||||
|
"outOfSync": "Out Of Sync",
|
||||||
|
"healthy": "Healthy",
|
||||||
|
"degraded": "Degraded",
|
||||||
|
"progressing": "Progressing",
|
||||||
|
"missing": "Missing",
|
||||||
|
"suspended": "Suspended"
|
||||||
|
},
|
||||||
|
"spoolman": {
|
||||||
|
"loading": "Loading"
|
||||||
|
},
|
||||||
|
"gitlab": {
|
||||||
|
"groups": "Groups",
|
||||||
|
"issues": "Issues",
|
||||||
|
"merges": "Merge Requests",
|
||||||
|
"projects": "Projects"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ export default function BookmarksGroup({ bookmarks, layout, disableCollapse, gro
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
"bookmark-group",
|
"bookmark-group",
|
||||||
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/4 lg:basis-1/5 xl:basis-1/6",
|
layout?.style === "row" ? "basis-full" : "basis-full md:basis-1/4 lg:basis-1/5 xl:basis-1/6",
|
||||||
layout?.header === false ? "flex-1 px-1 -my-1" : "flex-1 p-1",
|
layout?.header === false ? "flex-1 px-1 -my-1 overflow-hidden" : "flex-1 p-1 overflow-hidden",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Disclosure defaultOpen={!(layout?.initiallyCollapsed ?? groupsInitiallyCollapsed) ?? true}>
|
<Disclosure defaultOpen={!(layout?.initiallyCollapsed ?? groupsInitiallyCollapsed) ?? true}>
|
||||||
|
@ -29,9 +29,9 @@ export default function Item({ bookmark }) {
|
|||||||
)}
|
)}
|
||||||
{!bookmark.icon && bookmark.abbr}
|
{!bookmark.icon && bookmark.abbr}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex items-center justify-between rounded-r-md bookmark-text">
|
<div className="flex-1 overflow-hidden flex items-center justify-between rounded-r-md bookmark-text">
|
||||||
<div className="flex-1 grow pl-3 py-2 text-xs bookmark-name">{bookmark.name}</div>
|
<div className="pl-3 py-2 text-xs bookmark-name">{bookmark.name}</div>
|
||||||
<div className="px-2 py-2 truncate text-theme-500 dark:text-theme-300 text-xs bookmark-description">
|
<div className="shrink truncate px-2 py-2 text-theme-500 dark:text-theme-300 text-xs bookmark-description">
|
||||||
{description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -98,6 +98,12 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
|||||||
} else if (event.key === "ArrowUp" && currentItemIndex > 0) {
|
} else if (event.key === "ArrowUp" && currentItemIndex > 0) {
|
||||||
setCurrentItemIndex(currentItemIndex - 1);
|
setCurrentItemIndex(currentItemIndex - 1);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
} else if (
|
||||||
|
event.key === "ArrowRight" &&
|
||||||
|
results[currentItemIndex] &&
|
||||||
|
results[currentItemIndex].type === "searchSuggestion"
|
||||||
|
) {
|
||||||
|
setSearchString(results[currentItemIndex].name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ export default function Uptime({ refresh = 1500 }) {
|
|||||||
return (
|
return (
|
||||||
<Resource
|
<Resource
|
||||||
icon={FaRegClock}
|
icon={FaRegClock}
|
||||||
value={t("common.uptime", { value: data.uptime })}
|
value={t("common.duration", { value: data.uptime })}
|
||||||
label={t("resources.uptime")}
|
label={t("resources.uptime")}
|
||||||
percentage={percent}
|
percentage={percent}
|
||||||
/>
|
/>
|
||||||
|
@ -8,7 +8,7 @@ export default function Document() {
|
|||||||
name="description"
|
name="description"
|
||||||
content="A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations."
|
content="A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations."
|
||||||
/>
|
/>
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<link rel="manifest" href="/site.webmanifest?v=4" crossOrigin="use-credentials" />
|
<link rel="manifest" href="/site.webmanifest?v=4" crossOrigin="use-credentials" />
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -2,7 +2,7 @@ import cachedFetch from "utils/proxy/cached-fetch";
|
|||||||
|
|
||||||
export default async function handler(req, res) {
|
export default async function handler(req, res) {
|
||||||
const { latitude, longitude, units, cache, timezone } = req.query;
|
const { latitude, longitude, units, cache, timezone } = req.query;
|
||||||
const degrees = units === "imperial" ? "fahrenheit" : "celsius";
|
const degrees = units === "metric" ? "celsius" : "fahrenheit";
|
||||||
const timezeone = timezone ?? "auto";
|
const timezeone = timezone ?? "auto";
|
||||||
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset¤t_weather=true&temperature_unit=${degrees}&timezone=${timezeone}`;
|
const apiUrl = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=sunrise,sunset¤t_weather=true&temperature_unit=${degrees}&timezone=${timezeone}`;
|
||||||
return res.send(await cachedFetch(apiUrl, cache));
|
return res.send(await cachedFetch(apiUrl, cache));
|
||||||
|
@ -368,6 +368,9 @@ export function cleanServiceGroups(groups) {
|
|||||||
repositoryId,
|
repositoryId,
|
||||||
userEmail,
|
userEmail,
|
||||||
|
|
||||||
|
// beszel
|
||||||
|
systemId,
|
||||||
|
|
||||||
// calendar
|
// calendar
|
||||||
firstDayInWeek,
|
firstDayInWeek,
|
||||||
integrations,
|
integrations,
|
||||||
@ -415,7 +418,7 @@ export function cleanServiceGroups(groups) {
|
|||||||
pointsLimit,
|
pointsLimit,
|
||||||
diskUnits,
|
diskUnits,
|
||||||
|
|
||||||
// glances, customapi, iframe
|
// glances, customapi, iframe, prometheusmetric
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
|
|
||||||
// hdhomerun
|
// hdhomerun
|
||||||
@ -458,6 +461,9 @@ export function cleanServiceGroups(groups) {
|
|||||||
// opnsense, pfsense
|
// opnsense, pfsense
|
||||||
wan,
|
wan,
|
||||||
|
|
||||||
|
// prometheusmetric
|
||||||
|
metrics,
|
||||||
|
|
||||||
// proxmox
|
// proxmox
|
||||||
node,
|
node,
|
||||||
|
|
||||||
@ -486,6 +492,9 @@ export function cleanServiceGroups(groups) {
|
|||||||
|
|
||||||
// technitium
|
// technitium
|
||||||
range,
|
range,
|
||||||
|
|
||||||
|
// spoolman
|
||||||
|
spoolIds,
|
||||||
} = cleanedService.widget;
|
} = cleanedService.widget;
|
||||||
|
|
||||||
let fieldsList = fields;
|
let fieldsList = fields;
|
||||||
@ -511,6 +520,10 @@ export function cleanServiceGroups(groups) {
|
|||||||
if (repositoryId) cleanedService.widget.repositoryId = repositoryId;
|
if (repositoryId) cleanedService.widget.repositoryId = repositoryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type === "beszel") {
|
||||||
|
if (systemId) cleanedService.widget.systemId = systemId;
|
||||||
|
}
|
||||||
|
|
||||||
if (type === "coinmarketcap") {
|
if (type === "coinmarketcap") {
|
||||||
if (currency) cleanedService.widget.currency = currency;
|
if (currency) cleanedService.widget.currency = currency;
|
||||||
if (symbols) cleanedService.widget.symbols = symbols;
|
if (symbols) cleanedService.widget.symbols = symbols;
|
||||||
@ -639,6 +652,13 @@ export function cleanServiceGroups(groups) {
|
|||||||
if (type === "vikunja") {
|
if (type === "vikunja") {
|
||||||
if (enableTaskList !== undefined) cleanedService.widget.enableTaskList = !!enableTaskList;
|
if (enableTaskList !== undefined) cleanedService.widget.enableTaskList = !!enableTaskList;
|
||||||
}
|
}
|
||||||
|
if (type === "prometheusmetric") {
|
||||||
|
if (metrics) cleanedService.widget.metrics = metrics;
|
||||||
|
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
|
||||||
|
}
|
||||||
|
if (type === "spoolman") {
|
||||||
|
if (spoolIds !== undefined) cleanedService.widget.spoolIds = spoolIds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanedService;
|
return cleanedService;
|
||||||
|
@ -36,9 +36,11 @@ export default async function credentialedProxyHandler(req, res, map) {
|
|||||||
headers["X-gotify-Key"] = `${widget.key}`;
|
headers["X-gotify-Key"] = `${widget.key}`;
|
||||||
} else if (
|
} else if (
|
||||||
[
|
[
|
||||||
|
"argocd",
|
||||||
"authentik",
|
"authentik",
|
||||||
"cloudflared",
|
"cloudflared",
|
||||||
"ghostfolio",
|
"ghostfolio",
|
||||||
|
"headscale",
|
||||||
"linkwarden",
|
"linkwarden",
|
||||||
"mealie",
|
"mealie",
|
||||||
"netalertx",
|
"netalertx",
|
||||||
@ -92,6 +94,8 @@ export default async function credentialedProxyHandler(req, res, map) {
|
|||||||
}
|
}
|
||||||
} else if (widget.type === "wgeasy") {
|
} else if (widget.type === "wgeasy") {
|
||||||
headers.Authorization = widget.password;
|
headers.Authorization = widget.password;
|
||||||
|
} else if (widget.type === "gitlab") {
|
||||||
|
headers["PRIVATE-TOKEN"] = widget.key;
|
||||||
} else {
|
} else {
|
||||||
headers["X-API-Key"] = `${widget.key}`;
|
headers["X-API-Key"] = `${widget.key}`;
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ export default async function genericProxyHandler(req, res, map) {
|
|||||||
formatApiCall(widgets[widget.type].api, { endpoint, ...widget }).replace(/(?<=\?.*)\?/g, "&"),
|
formatApiCall(widgets[widget.type].api, { endpoint, ...widget }).replace(/(?<=\?.*)\?/g, "&"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const headers = req.extraHeaders ?? widget.headers ?? {};
|
const headers = req.extraHeaders ?? widget.headers ?? widgets[widget.type].headers ?? {};
|
||||||
|
|
||||||
if (widget.username && widget.password) {
|
if (widget.username && widget.password) {
|
||||||
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
|
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
|
||||||
@ -75,7 +75,13 @@ export default async function genericProxyHandler(req, res, map) {
|
|||||||
url.port ? `:${url.port}` : "",
|
url.port ? `:${url.port}` : "",
|
||||||
url.pathname,
|
url.pathname,
|
||||||
);
|
);
|
||||||
return res.status(status).json({ error: { message: "HTTP Error", url: sanitizeErrorURL(url), resultData } });
|
return res.status(status).json({
|
||||||
|
error: {
|
||||||
|
message: "HTTP Error",
|
||||||
|
url: sanitizeErrorURL(url),
|
||||||
|
resultData: Buffer.isBuffer(resultData) ? Buffer.from(resultData).toString() : resultData,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(status).send(resultData);
|
return res.status(status).send(resultData);
|
||||||
|
52
src/widgets/argocd/component.jsx
Normal file
52
src/widgets/argocd/component.jsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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 { widget } = service;
|
||||||
|
|
||||||
|
if (!widget.fields) {
|
||||||
|
widget.fields = ["apps", "synced", "outOfSync", "healthy"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_ALLOWED_FIELDS = 4;
|
||||||
|
if (widget.fields.length > MAX_ALLOWED_FIELDS) {
|
||||||
|
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: appsData, error: appsError } = useWidgetAPI(widget, "applications");
|
||||||
|
|
||||||
|
const appCounts = widget.fields.map((status) => {
|
||||||
|
if (status === "apps") {
|
||||||
|
return { status, count: appsData?.items?.length };
|
||||||
|
}
|
||||||
|
const count = appsData?.items?.filter(
|
||||||
|
(item) =>
|
||||||
|
item.status?.sync?.status.toLowerCase() === status.toLowerCase() ||
|
||||||
|
item.status?.health?.status.toLowerCase() === status.toLowerCase(),
|
||||||
|
).length;
|
||||||
|
return { status, count };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (appsError) {
|
||||||
|
return <Container service={service} error={appsError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appsData) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
{appCounts.map((a) => (
|
||||||
|
<Block label={`argocd.${a.status}`} key={a.status} />
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
{appCounts.map((a) => (
|
||||||
|
<Block label={`argocd.${a.status}`} key={a.status} value={a.count} />
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
14
src/widgets/argocd/widget.js
Normal file
14
src/widgets/argocd/widget.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/api/v1/{endpoint}",
|
||||||
|
proxyHandler: credentialedProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
applications: {
|
||||||
|
endpoint: "applications",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
@ -39,21 +39,15 @@ export default function Component({ service }) {
|
|||||||
<Block label="audiobookshelf.podcasts" value={t("common.number", { value: totalPodcasts })} />
|
<Block label="audiobookshelf.podcasts" value={t("common.number", { value: totalPodcasts })} />
|
||||||
<Block
|
<Block
|
||||||
label="audiobookshelf.podcastsDuration"
|
label="audiobookshelf.podcastsDuration"
|
||||||
value={t("common.number", {
|
value={t("common.duration", {
|
||||||
value: totalPodcastsDuration / 60,
|
value: totalPodcastsDuration,
|
||||||
maximumFractionDigits: 0,
|
|
||||||
style: "unit",
|
|
||||||
unit: "minute",
|
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
<Block label="audiobookshelf.books" value={t("common.number", { value: totalBooks })} />
|
<Block label="audiobookshelf.books" value={t("common.number", { value: totalBooks })} />
|
||||||
<Block
|
<Block
|
||||||
label="audiobookshelf.booksDuration"
|
label="audiobookshelf.booksDuration"
|
||||||
value={t("common.number", {
|
value={t("common.duration", {
|
||||||
value: totalBooksDuration / 60,
|
value: totalBooksDuration,
|
||||||
maximumFractionDigits: 0,
|
|
||||||
style: "unit",
|
|
||||||
unit: "minute",
|
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
|
60
src/widgets/beszel/component.jsx
Normal file
60
src/widgets/beszel/component.jsx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
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 { systemId } = widget;
|
||||||
|
|
||||||
|
const { data: systems, error: systemsError } = useWidgetAPI(widget, "systems");
|
||||||
|
|
||||||
|
const MAX_ALLOWED_FIELDS = 4;
|
||||||
|
if (!widget.fields?.length > 0) {
|
||||||
|
widget.fields = systemId ? ["name", "status", "cpu", "memory"] : ["systems", "up"];
|
||||||
|
}
|
||||||
|
if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
|
||||||
|
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemsError) {
|
||||||
|
return <Container service={service} error={systemsError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!systems) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="beszel.systems" />
|
||||||
|
<Block label="beszel.up" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemId) {
|
||||||
|
const system = systems.items.find((item) => item.id === systemId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="beszel.name" value={system.name} />
|
||||||
|
<Block label="beszel.status" value={t(`beszel.${system.status}`)} />
|
||||||
|
<Block label="beszel.updated" value={t("common.relativeDate", { value: system.updated })} />
|
||||||
|
<Block label="beszel.cpu" value={t("common.percent", { value: system.info.cpu, maximumFractionDigits: 2 })} />
|
||||||
|
<Block label="beszel.memory" value={t("common.percent", { value: system.info.mp, maximumFractionDigits: 2 })} />
|
||||||
|
<Block label="beszel.disk" value={t("common.percent", { value: system.info.dp, maximumFractionDigits: 2 })} />
|
||||||
|
<Block label="beszel.network" value={t("common.percent", { value: system.info.b, maximumFractionDigits: 2 })} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const upTotal = systems.items.filter((item) => item.status === "up").length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="beszel.systems" value={systems.totalItems} />
|
||||||
|
<Block label="beszel.up" value={`${upTotal} / ${systems.totalItems}`} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
99
src/widgets/beszel/proxy.js
Normal file
99
src/widgets/beszel/proxy.js
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import cache from "memory-cache";
|
||||||
|
|
||||||
|
import getServiceWidget from "utils/config/service-helpers";
|
||||||
|
import { formatApiCall } from "utils/proxy/api-helpers";
|
||||||
|
import { httpProxy } from "utils/proxy/http";
|
||||||
|
import widgets from "widgets/widgets";
|
||||||
|
import createLogger from "utils/logger";
|
||||||
|
|
||||||
|
const proxyName = "beszelProxyHandler";
|
||||||
|
const tokenCacheKey = `${proxyName}__token`;
|
||||||
|
const logger = createLogger(proxyName);
|
||||||
|
|
||||||
|
async function login(loginUrl, username, password, service) {
|
||||||
|
const authResponse = await httpProxy(loginUrl, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ identity: username, password }),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = authResponse[0];
|
||||||
|
let data = authResponse[2];
|
||||||
|
try {
|
||||||
|
data = JSON.parse(Buffer.from(authResponse[2]).toString());
|
||||||
|
|
||||||
|
if (status === 200) {
|
||||||
|
cache.put(`${tokenCacheKey}.${service}`, data.token);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Error ${status} logging into beszel`, JSON.stringify(authResponse[2]));
|
||||||
|
}
|
||||||
|
return [status, data.token ?? data];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function beszelProxyHandler(req, res) {
|
||||||
|
const { group, service, endpoint } = req.query;
|
||||||
|
|
||||||
|
if (group && service) {
|
||||||
|
const widget = await getServiceWidget(group, service);
|
||||||
|
|
||||||
|
if (!widgets?.[widget.type]?.api) {
|
||||||
|
return res.status(403).json({ error: "Service does not support API calls" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget) {
|
||||||
|
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
|
||||||
|
const loginUrl = formatApiCall(widgets[widget.type].api, { endpoint: "admins/auth-with-password", ...widget });
|
||||||
|
|
||||||
|
let status;
|
||||||
|
let data;
|
||||||
|
|
||||||
|
let token = cache.get(`${tokenCacheKey}.${service}`);
|
||||||
|
if (!token) {
|
||||||
|
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
||||||
|
if (status !== 200) {
|
||||||
|
logger.debug(`HTTP ${status} logging into npm api: ${token}`);
|
||||||
|
return res.status(status).send(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[status, , data] = await httpProxy(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status === 403) {
|
||||||
|
logger.debug(`HTTP ${status} retrieving data from npm api, logging in and trying again.`);
|
||||||
|
cache.del(`${tokenCacheKey}.${service}`);
|
||||||
|
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
logger.debug(`HTTP ${status} logging into npm api: ${data}`);
|
||||||
|
return res.status(status).send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
[status, , data] = await httpProxy(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
return res.status(status).send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.send(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).json({ error: "Invalid proxy service type" });
|
||||||
|
}
|
14
src/widgets/beszel/widget.js
Normal file
14
src/widgets/beszel/widget.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import beszelProxyHandler from "./proxy";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/api/{endpoint}",
|
||||||
|
proxyHandler: beszelProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
systems: {
|
||||||
|
endpoint: "collections/systems/records?page=1&perPage=500&sort=%2Bcreated",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
@ -21,7 +21,7 @@ export default async function calendarProxyHandler(req, res) {
|
|||||||
if (contentType) res.setHeader("Content-Type", contentType);
|
if (contentType) res.setHeader("Content-Type", contentType);
|
||||||
|
|
||||||
if (status !== 200) {
|
if (status !== 200) {
|
||||||
logger.debug(`HTTTP ${status} retrieving data from integration URL ${integration.url} : ${data}`);
|
logger.debug(`HTTP ${status} retrieving data from integration URL ${integration.url} : ${data}`);
|
||||||
return res.status(status).send(data);
|
return res.status(status).send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,12 +2,14 @@ import dynamic from "next/dynamic";
|
|||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
adguard: dynamic(() => import("./adguard/component")),
|
adguard: dynamic(() => import("./adguard/component")),
|
||||||
|
argocd: dynamic(() => import("./argocd/component")),
|
||||||
atsumeru: dynamic(() => import("./atsumeru/component")),
|
atsumeru: dynamic(() => import("./atsumeru/component")),
|
||||||
audiobookshelf: dynamic(() => import("./audiobookshelf/component")),
|
audiobookshelf: dynamic(() => import("./audiobookshelf/component")),
|
||||||
authentik: dynamic(() => import("./authentik/component")),
|
authentik: dynamic(() => import("./authentik/component")),
|
||||||
autobrr: dynamic(() => import("./autobrr/component")),
|
autobrr: dynamic(() => import("./autobrr/component")),
|
||||||
azuredevops: dynamic(() => import("./azuredevops/component")),
|
azuredevops: dynamic(() => import("./azuredevops/component")),
|
||||||
bazarr: dynamic(() => import("./bazarr/component")),
|
bazarr: dynamic(() => import("./bazarr/component")),
|
||||||
|
beszel: dynamic(() => import("./beszel/component")),
|
||||||
caddy: dynamic(() => import("./caddy/component")),
|
caddy: dynamic(() => import("./caddy/component")),
|
||||||
calendar: dynamic(() => import("./calendar/component")),
|
calendar: dynamic(() => import("./calendar/component")),
|
||||||
calibreweb: dynamic(() => import("./calibreweb/component")),
|
calibreweb: dynamic(() => import("./calibreweb/component")),
|
||||||
@ -36,11 +38,13 @@ const components = {
|
|||||||
gatus: dynamic(() => import("./gatus/component")),
|
gatus: dynamic(() => import("./gatus/component")),
|
||||||
ghostfolio: dynamic(() => import("./ghostfolio/component")),
|
ghostfolio: dynamic(() => import("./ghostfolio/component")),
|
||||||
gitea: dynamic(() => import("./gitea/component")),
|
gitea: dynamic(() => import("./gitea/component")),
|
||||||
|
gitlab: dynamic(() => import("./gitlab/component")),
|
||||||
glances: dynamic(() => import("./glances/component")),
|
glances: dynamic(() => import("./glances/component")),
|
||||||
gluetun: dynamic(() => import("./gluetun/component")),
|
gluetun: dynamic(() => import("./gluetun/component")),
|
||||||
gotify: dynamic(() => import("./gotify/component")),
|
gotify: dynamic(() => import("./gotify/component")),
|
||||||
grafana: dynamic(() => import("./grafana/component")),
|
grafana: dynamic(() => import("./grafana/component")),
|
||||||
hdhomerun: dynamic(() => import("./hdhomerun/component")),
|
hdhomerun: dynamic(() => import("./hdhomerun/component")),
|
||||||
|
headscale: dynamic(() => import("./headscale/component")),
|
||||||
peanut: dynamic(() => import("./peanut/component")),
|
peanut: dynamic(() => import("./peanut/component")),
|
||||||
homeassistant: dynamic(() => import("./homeassistant/component")),
|
homeassistant: dynamic(() => import("./homeassistant/component")),
|
||||||
homebox: dynamic(() => import("./homebox/component")),
|
homebox: dynamic(() => import("./homebox/component")),
|
||||||
@ -93,6 +97,7 @@ const components = {
|
|||||||
plex: dynamic(() => import("./plex/component")),
|
plex: dynamic(() => import("./plex/component")),
|
||||||
portainer: dynamic(() => import("./portainer/component")),
|
portainer: dynamic(() => import("./portainer/component")),
|
||||||
prometheus: dynamic(() => import("./prometheus/component")),
|
prometheus: dynamic(() => import("./prometheus/component")),
|
||||||
|
prometheusmetric: dynamic(() => import("./prometheusmetric/component")),
|
||||||
prowlarr: dynamic(() => import("./prowlarr/component")),
|
prowlarr: dynamic(() => import("./prowlarr/component")),
|
||||||
proxmox: dynamic(() => import("./proxmox/component")),
|
proxmox: dynamic(() => import("./proxmox/component")),
|
||||||
pterodactyl: dynamic(() => import("./pterodactyl/component")),
|
pterodactyl: dynamic(() => import("./pterodactyl/component")),
|
||||||
@ -107,10 +112,12 @@ const components = {
|
|||||||
scrutiny: dynamic(() => import("./scrutiny/component")),
|
scrutiny: dynamic(() => import("./scrutiny/component")),
|
||||||
sonarr: dynamic(() => import("./sonarr/component")),
|
sonarr: dynamic(() => import("./sonarr/component")),
|
||||||
speedtest: dynamic(() => import("./speedtest/component")),
|
speedtest: dynamic(() => import("./speedtest/component")),
|
||||||
|
spoolman: dynamic(() => import("./spoolman/component")),
|
||||||
stash: dynamic(() => import("./stash/component")),
|
stash: dynamic(() => import("./stash/component")),
|
||||||
stocks: dynamic(() => import("./stocks/component")),
|
stocks: dynamic(() => import("./stocks/component")),
|
||||||
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
||||||
swagdashboard: dynamic(() => import("./swagdashboard/component")),
|
swagdashboard: dynamic(() => import("./swagdashboard/component")),
|
||||||
|
suwayomi: dynamic(() => import("./suwayomi/component")),
|
||||||
tailscale: dynamic(() => import("./tailscale/component")),
|
tailscale: dynamic(() => import("./tailscale/component")),
|
||||||
tandoor: dynamic(() => import("./tandoor/component")),
|
tandoor: dynamic(() => import("./tandoor/component")),
|
||||||
tautulli: dynamic(() => import("./tautulli/component")),
|
tautulli: dynamic(() => import("./tautulli/component")),
|
||||||
|
@ -40,7 +40,7 @@ export default function Component({ service }) {
|
|||||||
/>
|
/>
|
||||||
<Block
|
<Block
|
||||||
label="frigate.uptime"
|
label="frigate.uptime"
|
||||||
value={t("common.uptime", {
|
value={t("common.duration", {
|
||||||
value: data.uptime,
|
value: data.uptime,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
@ -44,7 +44,7 @@ export default function Component({ service }) {
|
|||||||
return (
|
return (
|
||||||
<Container service={service}>
|
<Container service={service}>
|
||||||
<Block label="fritzbox.connectionStatus" value={t(`fritzbox.connectionStatus${fritzboxData.connectionStatus}`)} />
|
<Block label="fritzbox.connectionStatus" value={t(`fritzbox.connectionStatus${fritzboxData.connectionStatus}`)} />
|
||||||
<Block label="fritzbox.uptime" value={t("common.uptime", { value: fritzboxData.uptime })} />
|
<Block label="fritzbox.uptime" value={t("common.duration", { value: fritzboxData.uptime })} />
|
||||||
<Block label="fritzbox.maxDown" value={t("common.byterate", { value: fritzboxData.maxDown / 8, decimals: 1 })} />
|
<Block label="fritzbox.maxDown" value={t("common.byterate", { value: fritzboxData.maxDown / 8, decimals: 1 })} />
|
||||||
<Block label="fritzbox.maxUp" value={t("common.byterate", { value: fritzboxData.maxUp / 8, decimals: 1 })} />
|
<Block label="fritzbox.maxUp" value={t("common.byterate", { value: fritzboxData.maxUp / 8, decimals: 1 })} />
|
||||||
<Block label="fritzbox.down" value={t("common.byterate", { value: fritzboxData.down, decimals: 1 })} />
|
<Block label="fritzbox.down" value={t("common.byterate", { value: fritzboxData.down, decimals: 1 })} />
|
||||||
|
36
src/widgets/gitlab/component.jsx
Normal file
36
src/widgets/gitlab/component.jsx
Normal file
@ -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: gitlabCounts, error: gitlabCountsError } = useWidgetAPI(widget, "counts");
|
||||||
|
|
||||||
|
if (gitlabCountsError) {
|
||||||
|
return <Container service={service} error={gitlabCountsError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gitlabCounts) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="gitlab.groups" />
|
||||||
|
<Block label="gitlab.issues" />
|
||||||
|
<Block label="gitlab.merges" />
|
||||||
|
<Block label="gitlab.projects" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="gitlab.groups" value={t("common.number", { value: gitlabCounts.groups_count })} />
|
||||||
|
<Block label="gitlab.issues" value={t("common.number", { value: gitlabCounts.issues_count })} />
|
||||||
|
<Block label="gitlab.merges" value={t("common.number", { value: gitlabCounts.merge_requests_count })} />
|
||||||
|
<Block label="gitlab.projects" value={t("common.number", { value: gitlabCounts.projects_count })} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
13
src/widgets/gitlab/widget.js
Normal file
13
src/widgets/gitlab/widget.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/api/v4/{endpoint}",
|
||||||
|
proxyHandler: credentialedProxyHandler,
|
||||||
|
mappings: {
|
||||||
|
counts: {
|
||||||
|
endpoint: "users/{user_id}/associations_count",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
43
src/widgets/headscale/component.jsx
Normal file
43
src/widgets/headscale/component.jsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
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: nodeData, error: nodeError } = useWidgetAPI(widget, "node");
|
||||||
|
|
||||||
|
if (nodeError) {
|
||||||
|
return <Container service={service} error={nodeError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodeData) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="headscale.name" />
|
||||||
|
<Block label="headscale.address" />
|
||||||
|
<Block label="headscale.last_seen" />
|
||||||
|
<Block label="headscale.status" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
givenName,
|
||||||
|
ipAddresses: [address],
|
||||||
|
lastSeen,
|
||||||
|
online,
|
||||||
|
} = nodeData.node;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="headscale.name" value={givenName} />
|
||||||
|
<Block label="headscale.address" value={address} />
|
||||||
|
<Block label="headscale.last_seen" value={t("common.relativeDate", { value: lastSeen })} />
|
||||||
|
<Block label="headscale.status" value={t(online ? "headscale.online" : "headscale.offline")} />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
14
src/widgets/headscale/widget.js
Normal file
14
src/widgets/headscale/widget.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/api/v1/{endpoint}/{nodeId}",
|
||||||
|
proxyHandler: credentialedProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
node: {
|
||||||
|
endpoint: "node",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
@ -21,8 +21,8 @@ export default function Component({ service }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabled = infoData.filter((c) => c.enabled === 1).length;
|
const enabled = infoData.filter((c) => !!c.enabled).length;
|
||||||
const disabled = infoData.filter((c) => c.enabled === 0).length;
|
const disabled = infoData.filter((c) => !c.enabled).length;
|
||||||
const total = infoData.length;
|
const total = infoData.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -30,7 +30,7 @@ async function login(loginUrl, username, password, service) {
|
|||||||
cache.put(`${tokenCacheKey}.${service}`, data.token, expiration - 5 * 60 * 1000); // expiration -5 minutes
|
cache.put(`${tokenCacheKey}.${service}`, data.token, expiration - 5 * 60 * 1000); // expiration -5 minutes
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`Error ${status} logging into npm`, authResponse[2]);
|
logger.error(`Error ${status} logging into npm`, JSON.stringify(authResponse[2]));
|
||||||
}
|
}
|
||||||
return [status, data.token ?? data];
|
return [status, data.token ?? data];
|
||||||
}
|
}
|
||||||
@ -50,19 +50,18 @@ export default async function npmProxyHandler(req, res) {
|
|||||||
const loginUrl = `${widget.url}/api/tokens`;
|
const loginUrl = `${widget.url}/api/tokens`;
|
||||||
|
|
||||||
let status;
|
let status;
|
||||||
let contentType;
|
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
let token = cache.get(`${tokenCacheKey}.${service}`);
|
let token = cache.get(`${tokenCacheKey}.${service}`);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
||||||
if (status !== 200) {
|
if (status !== 200) {
|
||||||
logger.debug(`HTTTP ${status} logging into npm api: ${token}`);
|
logger.debug(`HTTP ${status} logging into npm api: ${token}`);
|
||||||
return res.status(status).send(token);
|
return res.status(status).send(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[status, contentType, data] = await httpProxy(url, {
|
[status, , data] = await httpProxy(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@ -71,17 +70,17 @@ export default async function npmProxyHandler(req, res) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (status === 403) {
|
if (status === 403) {
|
||||||
logger.debug(`HTTTP ${status} retrieving data from npm api, logging in and trying again.`);
|
logger.debug(`HTTP ${status} retrieving data from npm api, logging in and trying again.`);
|
||||||
cache.del(`${tokenCacheKey}.${service}`);
|
cache.del(`${tokenCacheKey}.${service}`);
|
||||||
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
||||||
|
|
||||||
if (status !== 200) {
|
if (status !== 200) {
|
||||||
logger.debug(`HTTTP ${status} logging into npm api: ${data}`);
|
logger.debug(`HTTP ${status} logging into npm api: ${data}`);
|
||||||
return res.status(status).send(data);
|
return res.status(status).send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
[status, contentType, data] = await httpProxy(url, {
|
[status, , data] = await httpProxy(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
@ -138,7 +138,7 @@ export default async function omadaProxyHandler(req, res) {
|
|||||||
const sitesResponseData = JSON.parse(data);
|
const sitesResponseData = JSON.parse(data);
|
||||||
|
|
||||||
if (status !== 200 || sitesResponseData.errorCode > 0) {
|
if (status !== 200 || sitesResponseData.errorCode > 0) {
|
||||||
logger.debug(`HTTTP ${status} getting sites list: ${sitesResponseData.msg}`);
|
logger.debug(`HTTP ${status} getting sites list: ${sitesResponseData.msg}`);
|
||||||
return res
|
return res
|
||||||
.status(status)
|
.status(status)
|
||||||
.json({ error: { message: "Error getting sites list", url, data: sitesResponseData } });
|
.json({ error: { message: "Error getting sites list", url, data: sitesResponseData } });
|
||||||
|
@ -20,7 +20,7 @@ export default function Component({ service }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container service={service}>
|
<Container service={service}>
|
||||||
<Block label="openwrt.uptime" value={t("common.uptime", { value: uptime })} />
|
<Block label="openwrt.uptime" value={t("common.duration", { value: uptime })} />
|
||||||
<Block label="openwrt.cpuLoad" value={cpuLoad} />
|
<Block label="openwrt.cpuLoad" value={cpuLoad} />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
116
src/widgets/prometheusmetric/component.jsx
Normal file
116
src/widgets/prometheusmetric/component.jsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
|
import Container from "components/services/widget/container";
|
||||||
|
import Block from "components/services/widget/block";
|
||||||
|
import useWidgetAPI from "utils/proxy/use-widget-api";
|
||||||
|
|
||||||
|
function formatValue(t, metric, rawValue) {
|
||||||
|
if (!metric?.format) return rawValue;
|
||||||
|
if (!rawValue) return "-";
|
||||||
|
|
||||||
|
let value = rawValue;
|
||||||
|
|
||||||
|
// Scale the value. Accepts either a number to multiply by or a string
|
||||||
|
// like "12/345".
|
||||||
|
const scale = metric?.format?.scale;
|
||||||
|
if (typeof scale === "number") {
|
||||||
|
value *= scale;
|
||||||
|
} else if (typeof scale === "string" && scale.includes("/")) {
|
||||||
|
const parts = scale.split("/");
|
||||||
|
const numerator = parts[0] ? parseFloat(parts[0]) : 1;
|
||||||
|
const denominator = parts[1] ? parseFloat(parts[1]) : 1;
|
||||||
|
value = (value * numerator) / denominator;
|
||||||
|
} else {
|
||||||
|
value = parseFloat(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the value using a known type and optional options.
|
||||||
|
switch (metric?.format?.type) {
|
||||||
|
case "text":
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
value = t(`common.${metric.format.type}`, { value, ...metric.format?.options });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply fixed prefix.
|
||||||
|
const prefix = metric?.format?.prefix;
|
||||||
|
if (prefix) {
|
||||||
|
value = `${prefix}${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply fixed suffix.
|
||||||
|
const suffix = metric?.format?.suffix;
|
||||||
|
if (suffix) {
|
||||||
|
value = `${value}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { widget } = service;
|
||||||
|
|
||||||
|
const { metrics = [], refreshInterval = 10000 } = widget;
|
||||||
|
|
||||||
|
let prometheusmetricError;
|
||||||
|
|
||||||
|
const prometheusmetricData = new Map(
|
||||||
|
metrics.slice(0, 4).map((metric) => {
|
||||||
|
// disable the rule that hooks should not be called from a callback,
|
||||||
|
// because we don't need a strong guarantee of hook execution order here.
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const { data: resultData, error: resultError } = useWidgetAPI(widget, "query", {
|
||||||
|
query: metric.query,
|
||||||
|
refreshInterval: Math.max(1000, metric.refreshInterval ?? refreshInterval),
|
||||||
|
});
|
||||||
|
if (resultError) {
|
||||||
|
prometheusmetricError = resultError;
|
||||||
|
}
|
||||||
|
return [metric.key ?? metric.label, resultData];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (prometheusmetricError) {
|
||||||
|
return <Container service={service} error={prometheusmetricError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prometheusmetricData) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
{metrics.slice(0, 4).map((item) => (
|
||||||
|
<Block label={item.label} key={item.label} />
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResultValue(data) {
|
||||||
|
// Fetches the first metric result from the Prometheus query result data.
|
||||||
|
// The first element in the result value is the timestamp which is ignored here.
|
||||||
|
const resultType = data?.data?.resultType;
|
||||||
|
const result = data?.data?.result;
|
||||||
|
|
||||||
|
switch (resultType) {
|
||||||
|
case "vector":
|
||||||
|
return result?.[0]?.value?.[1];
|
||||||
|
case "scalar":
|
||||||
|
return result?.[1];
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
{metrics.map((metric) => (
|
||||||
|
<Block
|
||||||
|
label={metric.label}
|
||||||
|
key={metric.key ?? metric.label}
|
||||||
|
value={formatValue(t, metric, getResultValue(prometheusmetricData.get(metric.key ?? metric.label)))}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
16
src/widgets/prometheusmetric/widget.js
Normal file
16
src/widgets/prometheusmetric/widget.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/api/v1/{endpoint}",
|
||||||
|
proxyHandler: genericProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
query: {
|
||||||
|
method: "GET",
|
||||||
|
endpoint: "query",
|
||||||
|
params: ["query"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
@ -15,7 +15,7 @@ async function fetchFromPyloadAPI(url, sessionId, params, service) {
|
|||||||
const options = {
|
const options = {
|
||||||
body: params
|
body: params
|
||||||
? Object.keys(params)
|
? Object.keys(params)
|
||||||
.map((prop) => `${prop}=${params[prop]}`)
|
.map((prop) => `${prop}=${encodeURIComponent(params[prop])}`)
|
||||||
.join("&")
|
.join("&")
|
||||||
: `session=${sessionId}`,
|
: `session=${sessionId}`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -12,7 +12,10 @@ const widget = {
|
|||||||
wanted: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile && item.isAvailable).length,
|
wanted: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile && item.isAvailable).length,
|
||||||
have: jsonArrayFilter(data, (item) => item.hasFile).length,
|
have: jsonArrayFilter(data, (item) => item.hasFile).length,
|
||||||
missing: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile).length,
|
missing: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile).length,
|
||||||
all: asJson(data),
|
all: asJson(data).map((entry) => ({
|
||||||
|
title: entry.title,
|
||||||
|
id: entry.id,
|
||||||
|
})),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
"queue/status": {
|
"queue/status": {
|
||||||
|
63
src/widgets/spoolman/component.jsx
Normal file
63
src/widgets/spoolman/component.jsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let { data: spoolData, error: spoolError } = useWidgetAPI(widget, "spools");
|
||||||
|
|
||||||
|
if (spoolError) {
|
||||||
|
return <Container service={service} error={spoolError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!spoolData) {
|
||||||
|
const nBlocksGuess = widget.spoolIds?.length ?? 4;
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
{[...Array(nBlocksGuess)].map((_, i) => (
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
<Block key={i} label="spoolman.loading" />
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spoolData.error || spoolData.message) {
|
||||||
|
return <Container service={service} error={spoolData?.error ?? spoolData} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spoolData.length === 0) {
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
<Block label="spoolman.noSpools" />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.spoolIds?.length) {
|
||||||
|
spoolData = spoolData.filter((spool) => widget.spoolIds.includes(spool.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spoolData.length > 4) {
|
||||||
|
spoolData = spoolData.slice(0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
{spoolData.map((spool) => (
|
||||||
|
<Block
|
||||||
|
key={spool.id}
|
||||||
|
label={spool.filament.name}
|
||||||
|
value={t("common.percent", {
|
||||||
|
value: (spool.remaining_weight / spool.initial_weight) * 100,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
14
src/widgets/spoolman/widget.js
Normal file
14
src/widgets/spoolman/widget.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/api/v1/{endpoint}",
|
||||||
|
proxyHandler: credentialedProxyHandler,
|
||||||
|
|
||||||
|
mappings: {
|
||||||
|
spools: {
|
||||||
|
endpoint: "spool",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
@ -46,12 +46,12 @@ export default function Component({ service }) {
|
|||||||
<Block label="stash.scenes" value={t("common.number", { value: stats.scene_count })} />
|
<Block label="stash.scenes" value={t("common.number", { value: stats.scene_count })} />
|
||||||
<Block label="stash.scenesPlayed" value={t("common.number", { value: stats.scenes_played })} />
|
<Block label="stash.scenesPlayed" value={t("common.number", { value: stats.scenes_played })} />
|
||||||
<Block label="stash.playCount" value={t("common.number", { value: stats.total_play_count })} />
|
<Block label="stash.playCount" value={t("common.number", { value: stats.total_play_count })} />
|
||||||
<Block label="stash.playDuration" value={t("common.uptime", { value: stats.total_play_duration })} />
|
<Block label="stash.playDuration" value={t("common.duration", { value: stats.total_play_duration })} />
|
||||||
<Block
|
<Block
|
||||||
label="stash.sceneSize"
|
label="stash.sceneSize"
|
||||||
value={t("common.bbytes", { value: stats.scenes_size, maximumFractionDigits: 1 })}
|
value={t("common.bbytes", { value: stats.scenes_size, maximumFractionDigits: 1 })}
|
||||||
/>
|
/>
|
||||||
<Block label="stash.sceneDuration" value={t("common.uptime", { value: stats.scenes_duration })} />
|
<Block label="stash.sceneDuration" value={t("common.duration", { value: stats.scenes_duration })} />
|
||||||
|
|
||||||
<Block label="stash.images" value={t("common.number", { value: stats.image_count })} />
|
<Block label="stash.images" value={t("common.number", { value: stats.image_count })} />
|
||||||
<Block
|
<Block
|
||||||
|
40
src/widgets/suwayomi/component.jsx
Normal file
40
src/widgets/suwayomi/component.jsx
Normal file
@ -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: suwayomiData, error: suwayomiError } = useWidgetAPI(widget);
|
||||||
|
|
||||||
|
if (suwayomiError) {
|
||||||
|
return <Container service={service} error={suwayomiError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!suwayomiData) {
|
||||||
|
if (!widget.fields || widget.fields.length === 0) {
|
||||||
|
widget.fields = ["download", "nondownload", "read", "unread"];
|
||||||
|
} else if (widget.fields.length > 4) {
|
||||||
|
widget.fields = widget.fields.slice(0, 4);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
{widget.fields.map((field) => (
|
||||||
|
<Block key={field} label={`suwayomi.${field}`} />
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container service={service}>
|
||||||
|
{suwayomiData.map((data) => (
|
||||||
|
<Block key={data.label} label={data.label} value={t("common.number", { value: data.count })} />
|
||||||
|
))}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
175
src/widgets/suwayomi/proxy.js
Normal file
175
src/widgets/suwayomi/proxy.js
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
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 = "suwayomiProxyHandler";
|
||||||
|
const logger = createLogger(proxyName);
|
||||||
|
|
||||||
|
const countsToExtract = {
|
||||||
|
download: {
|
||||||
|
condition: (c) => c.isDownloaded,
|
||||||
|
gqlCondition: "isDownloaded: true",
|
||||||
|
},
|
||||||
|
nondownload: {
|
||||||
|
condition: (c) => !c.isDownloaded,
|
||||||
|
gqlCondition: "isDownloaded: false",
|
||||||
|
},
|
||||||
|
read: {
|
||||||
|
condition: (c) => c.isRead,
|
||||||
|
gqlCondition: "isRead: true",
|
||||||
|
},
|
||||||
|
unread: {
|
||||||
|
condition: (c) => !c.isRead,
|
||||||
|
gqlCondition: "isRead: false",
|
||||||
|
},
|
||||||
|
downloadedread: {
|
||||||
|
condition: (c) => c.isDownloaded && c.isRead,
|
||||||
|
gqlCondition: "isDownloaded: true, isRead: true",
|
||||||
|
},
|
||||||
|
downloadedunread: {
|
||||||
|
condition: (c) => c.isDownloaded && !c.isRead,
|
||||||
|
gqlCondition: "isDownloaded: true, isRead: false",
|
||||||
|
},
|
||||||
|
nondownloadedread: {
|
||||||
|
condition: (c) => !c.isDownloaded && c.isRead,
|
||||||
|
gqlCondition: "isDownloaded: false, isRead: true",
|
||||||
|
},
|
||||||
|
nondownloadedunread: {
|
||||||
|
condition: (c) => !c.isDownloaded && !c.isRead,
|
||||||
|
gqlCondition: "isDownloaded: false, isRead: false",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeBody(fields, category = "all") {
|
||||||
|
if (Number.isNaN(Number(category))) {
|
||||||
|
let query = "";
|
||||||
|
fields.forEach((field) => {
|
||||||
|
query += `
|
||||||
|
${field}: chapters(
|
||||||
|
condition: {${countsToExtract[field].gqlCondition}}
|
||||||
|
filter: {inLibrary: {equalTo: true}}
|
||||||
|
) {
|
||||||
|
totalCount
|
||||||
|
}`;
|
||||||
|
});
|
||||||
|
return JSON.stringify({
|
||||||
|
operationName: "Counts",
|
||||||
|
query: `
|
||||||
|
query Counts {
|
||||||
|
${query}
|
||||||
|
}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
operationName: "category",
|
||||||
|
query: `
|
||||||
|
query category($id: Int!) {
|
||||||
|
category(id: $id) {
|
||||||
|
# name
|
||||||
|
mangas {
|
||||||
|
nodes {
|
||||||
|
chapters {
|
||||||
|
nodes {
|
||||||
|
isRead
|
||||||
|
isDownloaded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
variables: {
|
||||||
|
id: Number(category),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractCounts(responseJSON, fields) {
|
||||||
|
if (!("category" in responseJSON.data)) {
|
||||||
|
return fields.map((field) => ({
|
||||||
|
count: responseJSON.data[field].totalCount,
|
||||||
|
label: `suwayomi.${field}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
const tmp = responseJSON.data.category.mangas.nodes.reduce(
|
||||||
|
(accumulator, manga) => {
|
||||||
|
manga.chapters.nodes.forEach((chapter) => {
|
||||||
|
fields.forEach((field, i) => {
|
||||||
|
if (countsToExtract[field].condition(chapter)) {
|
||||||
|
accumulator[i] += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return accumulator;
|
||||||
|
},
|
||||||
|
[0, 0, 0, 0],
|
||||||
|
);
|
||||||
|
return fields.map((field, i) => ({
|
||||||
|
count: tmp[i],
|
||||||
|
label: `suwayomi.${field}`,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function suwayomiProxyHandler(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" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!widget.fields || widget.fields.length === 0) {
|
||||||
|
widget.fields = ["download", "nondownload", "read", "unread"];
|
||||||
|
} else if (widget.fields.length > 4) {
|
||||||
|
widget.fields = widget.fields.slice(0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
|
||||||
|
|
||||||
|
const body = makeBody(widget.fields, widget.category);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (widget.username && widget.password) {
|
||||||
|
headers.Authorization = `Basic ${Buffer.from(`${widget.username}:${widget.password}`).toString("base64")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [status, contentType, data] = await httpProxy(url, {
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
logger.error("Invalid or missing username or password for service '%s' in group '%s'", service, group);
|
||||||
|
return res.status(status).send({ error: { message: "401: unauthorized, username or password is incorrect." } });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
logger.error(
|
||||||
|
"Error getting data from Suwayomi for service '%s' in group '%s': %d. Data: %s",
|
||||||
|
service,
|
||||||
|
group,
|
||||||
|
status,
|
||||||
|
data,
|
||||||
|
);
|
||||||
|
return res.status(status).send({ error: { message: "Error getting data. body: %s, data: %s", body, data } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const returnData = extractCounts(JSON.parse(data), widget.fields);
|
||||||
|
|
||||||
|
if (contentType) res.setHeader("Content-Type", contentType);
|
||||||
|
return res.status(status).send(returnData);
|
||||||
|
}
|
8
src/widgets/suwayomi/widget.js
Normal file
8
src/widgets/suwayomi/widget.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import suwayomiProxyHandler from "./proxy";
|
||||||
|
|
||||||
|
const widget = {
|
||||||
|
api: "{url}/api/graphql",
|
||||||
|
proxyHandler: suwayomiProxyHandler,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default widget;
|
@ -205,7 +205,7 @@ export default function Component({ service }) {
|
|||||||
<div className="flex flex-col pb-1 mx-1">
|
<div className="flex flex-col pb-1 mx-1">
|
||||||
{playing.map((session) => (
|
{playing.map((session) => (
|
||||||
<SessionEntry
|
<SessionEntry
|
||||||
key={session.Id}
|
key={session.session_key}
|
||||||
session={session}
|
session={session}
|
||||||
enableUser={enableUser}
|
enableUser={enableUser}
|
||||||
showEpisodeNumber={showEpisodeNumber}
|
showEpisodeNumber={showEpisodeNumber}
|
||||||
|
@ -35,7 +35,7 @@ export default function Component({ service }) {
|
|||||||
<>
|
<>
|
||||||
<Container service={service}>
|
<Container service={service}>
|
||||||
<Block label="truenas.load" value={t("common.number", { value: statusData.loadavg[0] })} />
|
<Block label="truenas.load" value={t("common.number", { value: statusData.loadavg[0] })} />
|
||||||
<Block label="truenas.uptime" value={t("common.uptime", { value: statusData.uptime_seconds })} />
|
<Block label="truenas.uptime" value={t("common.duration", { value: statusData.uptime_seconds })} />
|
||||||
<Block label="truenas.alerts" value={t("common.number", { value: alertData.pending })} />
|
<Block label="truenas.alerts" value={t("common.number", { value: alertData.pending })} />
|
||||||
</Container>
|
</Container>
|
||||||
{enablePools &&
|
{enablePools &&
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
|
||||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||||
|
|
||||||
const widget = {
|
const widget = {
|
||||||
|
@ -58,7 +58,7 @@ export default function Component({ service }) {
|
|||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
status = t("uptimerobot.up");
|
status = t("uptimerobot.up");
|
||||||
uptime = t("common.uptime", { value: monitor.logs[0].duration });
|
uptime = t("common.duration", { value: monitor.logs[0].duration });
|
||||||
logIndex = 1;
|
logIndex = 1;
|
||||||
break;
|
break;
|
||||||
case 8:
|
case 8:
|
||||||
@ -73,7 +73,7 @@ export default function Component({ service }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const lastDown = new Date(monitor.logs[logIndex].datetime * 1000).toLocaleString();
|
const lastDown = new Date(monitor.logs[logIndex].datetime * 1000).toLocaleString();
|
||||||
const downDuration = t("common.uptime", { value: monitor.logs[logIndex].duration });
|
const downDuration = t("common.duration", { value: monitor.logs[logIndex].duration });
|
||||||
const hideDown = logIndex === 1 && monitor.logs[logIndex].type !== 1;
|
const hideDown = logIndex === 1 && monitor.logs[logIndex].type !== 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import adguard from "./adguard/widget";
|
import adguard from "./adguard/widget";
|
||||||
|
import argocd from "./argocd/widget";
|
||||||
import atsumeru from "./atsumeru/widget";
|
import atsumeru from "./atsumeru/widget";
|
||||||
import audiobookshelf from "./audiobookshelf/widget";
|
import audiobookshelf from "./audiobookshelf/widget";
|
||||||
import authentik from "./authentik/widget";
|
import authentik from "./authentik/widget";
|
||||||
import autobrr from "./autobrr/widget";
|
import autobrr from "./autobrr/widget";
|
||||||
import azuredevops from "./azuredevops/widget";
|
import azuredevops from "./azuredevops/widget";
|
||||||
import bazarr from "./bazarr/widget";
|
import bazarr from "./bazarr/widget";
|
||||||
|
import beszel from "./beszel/widget";
|
||||||
import caddy from "./caddy/widget";
|
import caddy from "./caddy/widget";
|
||||||
import calendar from "./calendar/widget";
|
import calendar from "./calendar/widget";
|
||||||
import calibreweb from "./calibreweb/widget";
|
import calibreweb from "./calibreweb/widget";
|
||||||
@ -30,11 +32,13 @@ import gamedig from "./gamedig/widget";
|
|||||||
import gatus from "./gatus/widget";
|
import gatus from "./gatus/widget";
|
||||||
import ghostfolio from "./ghostfolio/widget";
|
import ghostfolio from "./ghostfolio/widget";
|
||||||
import gitea from "./gitea/widget";
|
import gitea from "./gitea/widget";
|
||||||
|
import gitlab from "./gitlab/widget";
|
||||||
import glances from "./glances/widget";
|
import glances from "./glances/widget";
|
||||||
import gluetun from "./gluetun/widget";
|
import gluetun from "./gluetun/widget";
|
||||||
import gotify from "./gotify/widget";
|
import gotify from "./gotify/widget";
|
||||||
import grafana from "./grafana/widget";
|
import grafana from "./grafana/widget";
|
||||||
import hdhomerun from "./hdhomerun/widget";
|
import hdhomerun from "./hdhomerun/widget";
|
||||||
|
import headscale from "./headscale/widget";
|
||||||
import homeassistant from "./homeassistant/widget";
|
import homeassistant from "./homeassistant/widget";
|
||||||
import homebox from "./homebox/widget";
|
import homebox from "./homebox/widget";
|
||||||
import homebridge from "./homebridge/widget";
|
import homebridge from "./homebridge/widget";
|
||||||
@ -85,6 +89,7 @@ import plantit from "./plantit/widget";
|
|||||||
import plex from "./plex/widget";
|
import plex from "./plex/widget";
|
||||||
import portainer from "./portainer/widget";
|
import portainer from "./portainer/widget";
|
||||||
import prometheus from "./prometheus/widget";
|
import prometheus from "./prometheus/widget";
|
||||||
|
import prometheusmetric from "./prometheusmetric/widget";
|
||||||
import prowlarr from "./prowlarr/widget";
|
import prowlarr from "./prowlarr/widget";
|
||||||
import proxmox from "./proxmox/widget";
|
import proxmox from "./proxmox/widget";
|
||||||
import pterodactyl from "./pterodactyl/widget";
|
import pterodactyl from "./pterodactyl/widget";
|
||||||
@ -98,10 +103,12 @@ import sabnzbd from "./sabnzbd/widget";
|
|||||||
import scrutiny from "./scrutiny/widget";
|
import scrutiny from "./scrutiny/widget";
|
||||||
import sonarr from "./sonarr/widget";
|
import sonarr from "./sonarr/widget";
|
||||||
import speedtest from "./speedtest/widget";
|
import speedtest from "./speedtest/widget";
|
||||||
|
import spoolman from "./spoolman/widget";
|
||||||
import stash from "./stash/widget";
|
import stash from "./stash/widget";
|
||||||
import stocks from "./stocks/widget";
|
import stocks from "./stocks/widget";
|
||||||
import strelaysrv from "./strelaysrv/widget";
|
import strelaysrv from "./strelaysrv/widget";
|
||||||
import swagdashboard from "./swagdashboard/widget";
|
import swagdashboard from "./swagdashboard/widget";
|
||||||
|
import suwayomi from "./suwayomi/widget";
|
||||||
import tailscale from "./tailscale/widget";
|
import tailscale from "./tailscale/widget";
|
||||||
import tandoor from "./tandoor/widget";
|
import tandoor from "./tandoor/widget";
|
||||||
import tautulli from "./tautulli/widget";
|
import tautulli from "./tautulli/widget";
|
||||||
@ -126,12 +133,14 @@ import zabbix from "./zabbix/widget";
|
|||||||
|
|
||||||
const widgets = {
|
const widgets = {
|
||||||
adguard,
|
adguard,
|
||||||
|
argocd,
|
||||||
atsumeru,
|
atsumeru,
|
||||||
audiobookshelf,
|
audiobookshelf,
|
||||||
authentik,
|
authentik,
|
||||||
autobrr,
|
autobrr,
|
||||||
azuredevops,
|
azuredevops,
|
||||||
bazarr,
|
bazarr,
|
||||||
|
beszel,
|
||||||
caddy,
|
caddy,
|
||||||
calibreweb,
|
calibreweb,
|
||||||
changedetectionio,
|
changedetectionio,
|
||||||
@ -156,11 +165,13 @@ const widgets = {
|
|||||||
gatus,
|
gatus,
|
||||||
ghostfolio,
|
ghostfolio,
|
||||||
gitea,
|
gitea,
|
||||||
|
gitlab,
|
||||||
glances,
|
glances,
|
||||||
gluetun,
|
gluetun,
|
||||||
gotify,
|
gotify,
|
||||||
grafana,
|
grafana,
|
||||||
hdhomerun,
|
hdhomerun,
|
||||||
|
headscale,
|
||||||
homeassistant,
|
homeassistant,
|
||||||
homebox,
|
homebox,
|
||||||
homebridge,
|
homebridge,
|
||||||
@ -214,6 +225,7 @@ const widgets = {
|
|||||||
plex,
|
plex,
|
||||||
portainer,
|
portainer,
|
||||||
prometheus,
|
prometheus,
|
||||||
|
prometheusmetric,
|
||||||
prowlarr,
|
prowlarr,
|
||||||
proxmox,
|
proxmox,
|
||||||
pterodactyl,
|
pterodactyl,
|
||||||
@ -228,10 +240,12 @@ const widgets = {
|
|||||||
scrutiny,
|
scrutiny,
|
||||||
sonarr,
|
sonarr,
|
||||||
speedtest,
|
speedtest,
|
||||||
|
spoolman,
|
||||||
stash,
|
stash,
|
||||||
stocks,
|
stocks,
|
||||||
strelaysrv,
|
strelaysrv,
|
||||||
swagdashboard,
|
swagdashboard,
|
||||||
|
suwayomi,
|
||||||
tailscale,
|
tailscale,
|
||||||
tandoor,
|
tandoor,
|
||||||
tautulli,
|
tautulli,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user