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:
|
||||
|
||||
env:
|
||||
# Use docker.io for Docker Hub if empty
|
||||
REGISTRY: ghcr.io
|
||||
# github.repository as <account>/<repo>
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
@ -66,14 +64,6 @@ jobs:
|
||||
- name: Checkout repository
|
||||
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
|
||||
# https://github.com/marketplace/actions/docker-setup-buildx#with-qemu
|
||||
- name: Setup QEMU
|
||||
@ -99,9 +89,15 @@ jobs:
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
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
|
||||
# https://github.com/docker/metadata-action
|
||||
@ -109,7 +105,9 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
images: |
|
||||
${{ env.IMAGE_NAME }}
|
||||
ghcr.io/${{ env.IMAGE_NAME }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
|
||||
@ -133,19 +131,6 @@ jobs:
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
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
|
||||
# https://github.com/docker/build-push-action/issues/252
|
||||
# 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.
|
||||
|
||||
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
|
||||
|
||||
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.date` | `2024-01-01` | Format a 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
|
||||
|
||||
|
@ -5,7 +5,11 @@ description: Unifi Controller Information Widget Configuration
|
||||
|
||||
_(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.
|
||||
|
||||
|
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.
|
||||
|
||||
- [Adguard Home](adguard-home.md)
|
||||
- [ArgoCD](argocd.md)
|
||||
- [Atsumeru](atsumeru.md)
|
||||
- [Audiobookshelf](audiobookshelf.md)
|
||||
- [Authentik](authentik.md)
|
||||
- [Autobrr](autobrr.md)
|
||||
- [Azure DevOps](azuredevops.md)
|
||||
- [Bazarr](bazarr.md)
|
||||
- [Beszel](beszel.md)
|
||||
- [Caddy](caddy.md)
|
||||
- [Calendar](calendar.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)
|
||||
- [Ghostfolio](ghostfolio.md)
|
||||
- [Gitea](gitea.md)
|
||||
- [Gitlab](gitlab.md)
|
||||
- [Glances](glances.md)
|
||||
- [Gluetun](gluetun.md)
|
||||
- [Gotify](gotify.md)
|
||||
- [Grafana](grafana.md)
|
||||
- [HDHomeRun](hdhomerun.md)
|
||||
- [Headscale](headscale.md)
|
||||
- [Healthchecks](healthchecks.md)
|
||||
- [Home Assistant](homeassistant.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)
|
||||
- [Portainer](portainer.md)
|
||||
- [Prometheus](prometheus.md)
|
||||
- [Prometheus Metric](prometheusmetric.md)
|
||||
- [Prowlarr](prowlarr.md)
|
||||
- [Proxmox](proxmox.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))_
|
||||
|
||||
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.
|
||||
|
||||
|
@ -31,12 +31,14 @@ nav:
|
||||
- "Service Widgets":
|
||||
- widgets/services/index.md
|
||||
- widgets/services/adguard-home.md
|
||||
- widgets/services/argocd.md
|
||||
- widgets/services/atsumeru.md
|
||||
- widgets/services/audiobookshelf.md
|
||||
- widgets/services/authentik.md
|
||||
- widgets/services/autobrr.md
|
||||
- widgets/services/azuredevops.md
|
||||
- widgets/services/bazarr.md
|
||||
- widgets/services/beszel.md
|
||||
- widgets/services/caddy.md
|
||||
- widgets/services/calendar.md
|
||||
- widgets/services/calibre-web.md
|
||||
@ -62,11 +64,13 @@ nav:
|
||||
- widgets/services/gatus.md
|
||||
- widgets/services/ghostfolio.md
|
||||
- widgets/services/gitea.md
|
||||
- widgets/services/gitlab.md
|
||||
- widgets/services/glances.md
|
||||
- widgets/services/gluetun.md
|
||||
- widgets/services/gotify.md
|
||||
- widgets/services/grafana.md
|
||||
- widgets/services/hdhomerun.md
|
||||
- widgets/services/headscale.md
|
||||
- widgets/services/healthchecks.md
|
||||
- widgets/services/homeassistant.md
|
||||
- widgets/services/homebox.md
|
||||
@ -119,6 +123,7 @@ nav:
|
||||
- widgets/services/plex.md
|
||||
- widgets/services/portainer.md
|
||||
- widgets/services/prometheus.md
|
||||
- widgets/services/prometheusmetric.md
|
||||
- widgets/services/prowlarr.md
|
||||
- widgets/services/proxmox.md
|
||||
- widgets/services/proxmoxbackupserver.md
|
||||
@ -134,6 +139,7 @@ nav:
|
||||
- widgets/services/scrutiny.md
|
||||
- widgets/services/sonarr.md
|
||||
- widgets/services/speedtest-tracker.md
|
||||
- widgets/services/spoolman.md
|
||||
- widgets/services/stash.md
|
||||
- widgets/services/stocks.md
|
||||
- widgets/services/swagdashboard.md
|
||||
|
@ -84,12 +84,12 @@ function prettyBytes(number, options) {
|
||||
return `${prefix + numberString} ${unit}`;
|
||||
}
|
||||
|
||||
function uptime(uptimeInSeconds, i18next) {
|
||||
const mo = Math.floor(uptimeInSeconds / (3600 * 24 * 31));
|
||||
const d = Math.floor((uptimeInSeconds % (3600 * 24 * 31)) / (3600 * 24));
|
||||
const h = Math.floor((uptimeInSeconds % (3600 * 24)) / 3600);
|
||||
const m = Math.floor((uptimeInSeconds % 3600) / 60);
|
||||
const s = Math.floor(uptimeInSeconds % 60);
|
||||
function duration(durationInSeconds, i18next) {
|
||||
const mo = Math.floor(durationInSeconds / (3600 * 24 * 31));
|
||||
const d = Math.floor((durationInSeconds % (3600 * 24 * 31)) / (3600 * 24));
|
||||
const h = Math.floor((durationInSeconds % (3600 * 24)) / 3600);
|
||||
const m = Math.floor((durationInSeconds % 3600) / 60);
|
||||
const s = Math.floor(durationInSeconds % 60);
|
||||
|
||||
const moDisplay = mo > 0 ? mo + i18next.t("common.months") : "";
|
||||
const dDisplay = d > 0 ? d + i18next.t("common.days") : "";
|
||||
@ -156,7 +156,7 @@ module.exports = {
|
||||
i18next.services.formatter.add("relativeDate", (value, 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",
|
||||
},
|
||||
|
46
package-lock.json
generated
46
package-lock.json
generated
@ -36,7 +36,7 @@
|
||||
"swr": "^1.3.0",
|
||||
"systeminformation": "^5.23.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"urbackup-server-api": "^0.52.0",
|
||||
"urbackup-server-api": "^0.52.1",
|
||||
"winston": "^3.11.0",
|
||||
"xml-js": "^1.6.11"
|
||||
},
|
||||
@ -47,7 +47,7 @@
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"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-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.37.1",
|
||||
@ -55,8 +55,8 @@
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwind-scrollbar": "^3.0.5",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.6.2"
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"osx-temperature-sensor": "^1.0.8"
|
||||
@ -3135,11 +3135,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-module-utils": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz",
|
||||
"integrity": "sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==",
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
|
||||
"integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^3.2.7"
|
||||
},
|
||||
@ -3163,11 +3162,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz",
|
||||
"integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==",
|
||||
"version": "2.31.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
|
||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.8",
|
||||
@ -3177,7 +3175,7 @@
|
||||
"debug": "^3.2.7",
|
||||
"doctrine": "^2.1.0",
|
||||
"eslint-import-resolver-node": "^0.3.9",
|
||||
"eslint-module-utils": "^2.9.0",
|
||||
"eslint-module-utils": "^2.12.0",
|
||||
"hasown": "^2.0.2",
|
||||
"is-core-module": "^2.15.1",
|
||||
"is-glob": "^4.0.3",
|
||||
@ -3186,13 +3184,14 @@
|
||||
"object.groupby": "^1.0.3",
|
||||
"object.values": "^1.2.0",
|
||||
"semver": "^6.3.1",
|
||||
"string.prototype.trimend": "^1.0.8",
|
||||
"tsconfig-paths": "^3.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"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": {
|
||||
@ -7713,9 +7712,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
|
||||
"integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
|
||||
"version": "3.4.14",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz",
|
||||
"integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
@ -8102,9 +8101,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
|
||||
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
|
||||
"version": "5.6.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@ -8186,10 +8185,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/urbackup-server-api": {
|
||||
"version": "0.52.0",
|
||||
"resolved": "https://registry.npmjs.org/urbackup-server-api/-/urbackup-server-api-0.52.0.tgz",
|
||||
"integrity": "sha512-KfroCFZEWCuCkWye1F1JwI2fkO1za/Mf1a8TNGTujzxU0ZGzDqhA1zCOcvV97q7nH1TKFNpw5tMZ06fSCKv2UA==",
|
||||
"license": "MIT",
|
||||
"version": "0.52.1",
|
||||
"resolved": "https://registry.npmjs.org/urbackup-server-api/-/urbackup-server-api-0.52.1.tgz",
|
||||
"integrity": "sha512-gAxF9MdXxnceqUr/1Uj2LuGZQb/bvZ3Ply9zH/UTSWGkwKL5C0qMPrBvKRyTHbPMG/NBuHF6BzavkF7GNvOLew==",
|
||||
"dependencies": {
|
||||
"async-mutex": "^0.5.0",
|
||||
"node-fetch": "^2.7.0"
|
||||
|
@ -38,7 +38,7 @@
|
||||
"swr": "^1.3.0",
|
||||
"systeminformation": "^5.23.2",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"urbackup-server-api": "^0.52.0",
|
||||
"urbackup-server-api": "^0.52.1",
|
||||
"winston": "^3.11.0",
|
||||
"xml-js": "^1.6.11"
|
||||
},
|
||||
@ -49,7 +49,7 @@
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"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-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.37.1",
|
||||
@ -57,8 +57,8 @@
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.2.5",
|
||||
"tailwind-scrollbar": "^3.0.5",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.6.2"
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"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
|
||||
version: 4.1.4
|
||||
urbackup-server-api:
|
||||
specifier: ^0.52.0
|
||||
version: 0.52.0
|
||||
specifier: ^0.52.1
|
||||
version: 0.52.1
|
||||
winston:
|
||||
specifier: ^3.11.0
|
||||
version: 3.14.2
|
||||
@ -108,7 +108,7 @@ importers:
|
||||
devDependencies:
|
||||
'@tailwindcss/forms':
|
||||
specifier: ^0.5.8
|
||||
version: 0.5.9(tailwindcss@3.4.13)
|
||||
version: 0.5.9(tailwindcss@3.4.14)
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.20(postcss@8.4.47)
|
||||
@ -117,16 +117,16 @@ importers:
|
||||
version: 8.57.1
|
||||
eslint-config-airbnb:
|
||||
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:
|
||||
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:
|
||||
specifier: ^9.1.0
|
||||
version: 9.1.0(eslint@8.57.1)
|
||||
eslint-plugin-import:
|
||||
specifier: ^2.29.1
|
||||
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)
|
||||
specifier: ^2.31.0
|
||||
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:
|
||||
specifier: ^6.8.0
|
||||
version: 6.10.0(eslint@8.57.1)
|
||||
@ -147,13 +147,13 @@ importers:
|
||||
version: 3.3.3
|
||||
tailwind-scrollbar:
|
||||
specifier: ^3.0.5
|
||||
version: 3.1.0(tailwindcss@3.4.13)
|
||||
version: 3.1.0(tailwindcss@3.4.14)
|
||||
tailwindcss:
|
||||
specifier: ^3.4.13
|
||||
version: 3.4.13
|
||||
specifier: ^3.4.14
|
||||
version: 3.4.14
|
||||
typescript:
|
||||
specifier: ^5.6.2
|
||||
version: 5.6.2
|
||||
specifier: ^5.6.3
|
||||
version: 5.6.3
|
||||
|
||||
packages:
|
||||
|
||||
@ -1077,6 +1077,27 @@ packages:
|
||||
eslint-plugin-import-x:
|
||||
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:
|
||||
resolution: {integrity: sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ==}
|
||||
engines: {node: '>=4'}
|
||||
@ -1098,12 +1119,12 @@ packages:
|
||||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
|
||||
eslint-plugin-import@2.30.0:
|
||||
resolution: {integrity: sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==}
|
||||
eslint-plugin-import@2.31.0:
|
||||
resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
'@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:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
@ -1151,6 +1172,7 @@ packages:
|
||||
eslint@8.57.1:
|
||||
resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==}
|
||||
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
|
||||
|
||||
espree@9.6.1:
|
||||
@ -2510,8 +2532,8 @@ packages:
|
||||
peerDependencies:
|
||||
tailwindcss: 3.x
|
||||
|
||||
tailwindcss@3.4.13:
|
||||
resolution: {integrity: sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==}
|
||||
tailwindcss@3.4.14:
|
||||
resolution: {integrity: sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@ -2624,8 +2646,8 @@ packages:
|
||||
resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
typescript@5.6.2:
|
||||
resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==}
|
||||
typescript@5.6.3:
|
||||
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
@ -2649,8 +2671,8 @@ packages:
|
||||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
urbackup-server-api@0.52.0:
|
||||
resolution: {integrity: sha512-KfroCFZEWCuCkWye1F1JwI2fkO1za/Mf1a8TNGTujzxU0ZGzDqhA1zCOcvV97q7nH1TKFNpw5tMZ06fSCKv2UA==}
|
||||
urbackup-server-api@0.52.1:
|
||||
resolution: {integrity: sha512-gAxF9MdXxnceqUr/1Uj2LuGZQb/bvZ3Ply9zH/UTSWGkwKL5C0qMPrBvKRyTHbPMG/NBuHF6BzavkF7GNvOLew==}
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
@ -2956,10 +2978,10 @@ snapshots:
|
||||
dependencies:
|
||||
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:
|
||||
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)':
|
||||
dependencies:
|
||||
@ -3015,13 +3037,13 @@ snapshots:
|
||||
|
||||
'@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:
|
||||
'@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/type-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.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.3)
|
||||
'@typescript-eslint/visitor-keys': 7.2.0
|
||||
debug: 4.3.6
|
||||
eslint: 8.57.1
|
||||
@ -3029,22 +3051,22 @@ snapshots:
|
||||
ignore: 5.3.2
|
||||
natural-compare: 1.4.0
|
||||
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:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@typescript-eslint/scope-manager': 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
|
||||
debug: 4.3.6
|
||||
eslint: 8.57.1
|
||||
optionalDependencies:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@ -3053,21 +3075,21 @@ snapshots:
|
||||
'@typescript-eslint/types': 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:
|
||||
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.6.2)
|
||||
'@typescript-eslint/utils': 7.2.0(eslint@8.57.1)(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.3)
|
||||
debug: 4.3.6
|
||||
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:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@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:
|
||||
'@typescript-eslint/types': 7.2.0
|
||||
'@typescript-eslint/visitor-keys': 7.2.0
|
||||
@ -3076,20 +3098,20 @@ snapshots:
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.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:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1)
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/semver': 7.5.8
|
||||
'@typescript-eslint/scope-manager': 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
|
||||
semver: 7.6.3
|
||||
transitivePeerDependencies:
|
||||
@ -3759,41 +3781,41 @@ snapshots:
|
||||
|
||||
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:
|
||||
confusing-browser-globals: 1.0.11
|
||||
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.entries: 1.1.8
|
||||
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:
|
||||
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-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-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.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-react: 7.37.1(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
|
||||
object.assign: 4.1.5
|
||||
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:
|
||||
'@next/eslint-plugin-next': 14.2.8
|
||||
'@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/parser': 7.2.0(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.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.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.30.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-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.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-react: 7.37.1(eslint@8.57.1)
|
||||
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
|
||||
optionalDependencies:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.6.3
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-webpack
|
||||
- eslint-plugin-import-x
|
||||
@ -3811,37 +3833,48 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- 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:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.3.6
|
||||
enhanced-resolve: 5.17.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
|
||||
get-tsconfig: 4.8.0
|
||||
is-bun-module: 1.1.0
|
||||
is-glob: 4.0.3
|
||||
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:
|
||||
- '@typescript-eslint/parser'
|
||||
- eslint-import-resolver-node
|
||||
- eslint-import-resolver-webpack
|
||||
- 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:
|
||||
debug: 3.2.7
|
||||
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-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:
|
||||
- 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:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.8
|
||||
@ -3852,7 +3885,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.57.1
|
||||
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
|
||||
is-core-module: 2.15.1
|
||||
is-glob: 4.0.3
|
||||
@ -3861,9 +3894,10 @@ snapshots:
|
||||
object.groupby: 1.0.3
|
||||
object.values: 1.2.0
|
||||
semver: 6.3.1
|
||||
string.prototype.trimend: 1.0.8
|
||||
tsconfig-paths: 3.15.0
|
||||
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:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
@ -5355,11 +5389,11 @@ snapshots:
|
||||
|
||||
systeminformation@5.23.5: {}
|
||||
|
||||
tailwind-scrollbar@3.1.0(tailwindcss@3.4.13):
|
||||
tailwind-scrollbar@3.1.0(tailwindcss@3.4.14):
|
||||
dependencies:
|
||||
tailwindcss: 3.4.13
|
||||
tailwindcss: 3.4.14
|
||||
|
||||
tailwindcss@3.4.13:
|
||||
tailwindcss@3.4.14:
|
||||
dependencies:
|
||||
'@alloc/quick-lru': 5.2.0
|
||||
arg: 5.0.2
|
||||
@ -5454,9 +5488,9 @@ snapshots:
|
||||
|
||||
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:
|
||||
typescript: 5.6.2
|
||||
typescript: 5.6.3
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
@ -5515,7 +5549,7 @@ snapshots:
|
||||
is-typed-array: 1.1.13
|
||||
possible-typed-array-names: 1.0.0
|
||||
|
||||
typescript@5.6.2: {}
|
||||
typescript@5.6.3: {}
|
||||
|
||||
unbox-primitive@1.0.2:
|
||||
dependencies:
|
||||
@ -5536,7 +5570,7 @@ snapshots:
|
||||
escalade: 3.2.0
|
||||
picocolors: 1.1.0
|
||||
|
||||
urbackup-server-api@0.52.0:
|
||||
urbackup-server-api@0.52.1:
|
||||
dependencies:
|
||||
async-mutex: 0.5.0
|
||||
node-fetch: 2.7.0
|
||||
|
@ -13,7 +13,7 @@
|
||||
"ms": "{{value, number}}",
|
||||
"date": "{{value, date}}",
|
||||
"relativeDate": "{{value, relativeDate}}",
|
||||
"uptime": "{{value, uptime}}",
|
||||
"duration": "{{value, duration}}",
|
||||
"months": "mo",
|
||||
"days": "d",
|
||||
"hours": "h",
|
||||
@ -309,6 +309,16 @@
|
||||
"stopped": "Stopped",
|
||||
"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": {
|
||||
"address": "Address",
|
||||
"expires": "Expires",
|
||||
@ -959,5 +969,43 @@
|
||||
"tasks7d": "Tasks Due This Week",
|
||||
"tasksOverdue": "Overdue Tasks",
|
||||
"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(
|
||||
"bookmark-group",
|
||||
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}>
|
||||
|
@ -29,9 +29,9 @@ export default function Item({ bookmark }) {
|
||||
)}
|
||||
{!bookmark.icon && bookmark.abbr}
|
||||
</div>
|
||||
<div className="flex-1 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="px-2 py-2 truncate text-theme-500 dark:text-theme-300 text-xs bookmark-description">
|
||||
<div className="flex-1 overflow-hidden flex items-center justify-between rounded-r-md bookmark-text">
|
||||
<div className="pl-3 py-2 text-xs bookmark-name">{bookmark.name}</div>
|
||||
<div className="shrink truncate px-2 py-2 text-theme-500 dark:text-theme-300 text-xs bookmark-description">
|
||||
{description}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -98,6 +98,12 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
|
||||
} else if (event.key === "ArrowUp" && currentItemIndex > 0) {
|
||||
setCurrentItemIndex(currentItemIndex - 1);
|
||||
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 (
|
||||
<Resource
|
||||
icon={FaRegClock}
|
||||
value={t("common.uptime", { value: data.uptime })}
|
||||
value={t("common.duration", { value: data.uptime })}
|
||||
label={t("resources.uptime")}
|
||||
percentage={percent}
|
||||
/>
|
||||
|
@ -8,7 +8,7 @@ export default function Document() {
|
||||
name="description"
|
||||
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" />
|
||||
</Head>
|
||||
<body>
|
||||
|
@ -2,7 +2,7 @@ import cachedFetch from "utils/proxy/cached-fetch";
|
||||
|
||||
export default async function handler(req, res) {
|
||||
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 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));
|
||||
|
@ -368,6 +368,9 @@ export function cleanServiceGroups(groups) {
|
||||
repositoryId,
|
||||
userEmail,
|
||||
|
||||
// beszel
|
||||
systemId,
|
||||
|
||||
// calendar
|
||||
firstDayInWeek,
|
||||
integrations,
|
||||
@ -415,7 +418,7 @@ export function cleanServiceGroups(groups) {
|
||||
pointsLimit,
|
||||
diskUnits,
|
||||
|
||||
// glances, customapi, iframe
|
||||
// glances, customapi, iframe, prometheusmetric
|
||||
refreshInterval,
|
||||
|
||||
// hdhomerun
|
||||
@ -458,6 +461,9 @@ export function cleanServiceGroups(groups) {
|
||||
// opnsense, pfsense
|
||||
wan,
|
||||
|
||||
// prometheusmetric
|
||||
metrics,
|
||||
|
||||
// proxmox
|
||||
node,
|
||||
|
||||
@ -486,6 +492,9 @@ export function cleanServiceGroups(groups) {
|
||||
|
||||
// technitium
|
||||
range,
|
||||
|
||||
// spoolman
|
||||
spoolIds,
|
||||
} = cleanedService.widget;
|
||||
|
||||
let fieldsList = fields;
|
||||
@ -511,6 +520,10 @@ export function cleanServiceGroups(groups) {
|
||||
if (repositoryId) cleanedService.widget.repositoryId = repositoryId;
|
||||
}
|
||||
|
||||
if (type === "beszel") {
|
||||
if (systemId) cleanedService.widget.systemId = systemId;
|
||||
}
|
||||
|
||||
if (type === "coinmarketcap") {
|
||||
if (currency) cleanedService.widget.currency = currency;
|
||||
if (symbols) cleanedService.widget.symbols = symbols;
|
||||
@ -639,6 +652,13 @@ export function cleanServiceGroups(groups) {
|
||||
if (type === "vikunja") {
|
||||
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;
|
||||
|
@ -36,9 +36,11 @@ export default async function credentialedProxyHandler(req, res, map) {
|
||||
headers["X-gotify-Key"] = `${widget.key}`;
|
||||
} else if (
|
||||
[
|
||||
"argocd",
|
||||
"authentik",
|
||||
"cloudflared",
|
||||
"ghostfolio",
|
||||
"headscale",
|
||||
"linkwarden",
|
||||
"mealie",
|
||||
"netalertx",
|
||||
@ -92,6 +94,8 @@ export default async function credentialedProxyHandler(req, res, map) {
|
||||
}
|
||||
} else if (widget.type === "wgeasy") {
|
||||
headers.Authorization = widget.password;
|
||||
} else if (widget.type === "gitlab") {
|
||||
headers["PRIVATE-TOKEN"] = widget.key;
|
||||
} else {
|
||||
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, "&"),
|
||||
);
|
||||
|
||||
const headers = req.extraHeaders ?? widget.headers ?? {};
|
||||
const headers = req.extraHeaders ?? widget.headers ?? widgets[widget.type].headers ?? {};
|
||||
|
||||
if (widget.username && widget.password) {
|
||||
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.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);
|
||||
|
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.podcastsDuration"
|
||||
value={t("common.number", {
|
||||
value: totalPodcastsDuration / 60,
|
||||
maximumFractionDigits: 0,
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
value={t("common.duration", {
|
||||
value: totalPodcastsDuration,
|
||||
})}
|
||||
/>
|
||||
<Block label="audiobookshelf.books" value={t("common.number", { value: totalBooks })} />
|
||||
<Block
|
||||
label="audiobookshelf.booksDuration"
|
||||
value={t("common.number", {
|
||||
value: totalBooksDuration / 60,
|
||||
maximumFractionDigits: 0,
|
||||
style: "unit",
|
||||
unit: "minute",
|
||||
value={t("common.duration", {
|
||||
value: totalBooksDuration,
|
||||
})}
|
||||
/>
|
||||
</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 (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);
|
||||
}
|
||||
|
||||
|
@ -2,12 +2,14 @@ import dynamic from "next/dynamic";
|
||||
|
||||
const components = {
|
||||
adguard: dynamic(() => import("./adguard/component")),
|
||||
argocd: dynamic(() => import("./argocd/component")),
|
||||
atsumeru: dynamic(() => import("./atsumeru/component")),
|
||||
audiobookshelf: dynamic(() => import("./audiobookshelf/component")),
|
||||
authentik: dynamic(() => import("./authentik/component")),
|
||||
autobrr: dynamic(() => import("./autobrr/component")),
|
||||
azuredevops: dynamic(() => import("./azuredevops/component")),
|
||||
bazarr: dynamic(() => import("./bazarr/component")),
|
||||
beszel: dynamic(() => import("./beszel/component")),
|
||||
caddy: dynamic(() => import("./caddy/component")),
|
||||
calendar: dynamic(() => import("./calendar/component")),
|
||||
calibreweb: dynamic(() => import("./calibreweb/component")),
|
||||
@ -36,11 +38,13 @@ const components = {
|
||||
gatus: dynamic(() => import("./gatus/component")),
|
||||
ghostfolio: dynamic(() => import("./ghostfolio/component")),
|
||||
gitea: dynamic(() => import("./gitea/component")),
|
||||
gitlab: dynamic(() => import("./gitlab/component")),
|
||||
glances: dynamic(() => import("./glances/component")),
|
||||
gluetun: dynamic(() => import("./gluetun/component")),
|
||||
gotify: dynamic(() => import("./gotify/component")),
|
||||
grafana: dynamic(() => import("./grafana/component")),
|
||||
hdhomerun: dynamic(() => import("./hdhomerun/component")),
|
||||
headscale: dynamic(() => import("./headscale/component")),
|
||||
peanut: dynamic(() => import("./peanut/component")),
|
||||
homeassistant: dynamic(() => import("./homeassistant/component")),
|
||||
homebox: dynamic(() => import("./homebox/component")),
|
||||
@ -93,6 +97,7 @@ const components = {
|
||||
plex: dynamic(() => import("./plex/component")),
|
||||
portainer: dynamic(() => import("./portainer/component")),
|
||||
prometheus: dynamic(() => import("./prometheus/component")),
|
||||
prometheusmetric: dynamic(() => import("./prometheusmetric/component")),
|
||||
prowlarr: dynamic(() => import("./prowlarr/component")),
|
||||
proxmox: dynamic(() => import("./proxmox/component")),
|
||||
pterodactyl: dynamic(() => import("./pterodactyl/component")),
|
||||
@ -107,10 +112,12 @@ const components = {
|
||||
scrutiny: dynamic(() => import("./scrutiny/component")),
|
||||
sonarr: dynamic(() => import("./sonarr/component")),
|
||||
speedtest: dynamic(() => import("./speedtest/component")),
|
||||
spoolman: dynamic(() => import("./spoolman/component")),
|
||||
stash: dynamic(() => import("./stash/component")),
|
||||
stocks: dynamic(() => import("./stocks/component")),
|
||||
strelaysrv: dynamic(() => import("./strelaysrv/component")),
|
||||
swagdashboard: dynamic(() => import("./swagdashboard/component")),
|
||||
suwayomi: dynamic(() => import("./suwayomi/component")),
|
||||
tailscale: dynamic(() => import("./tailscale/component")),
|
||||
tandoor: dynamic(() => import("./tandoor/component")),
|
||||
tautulli: dynamic(() => import("./tautulli/component")),
|
||||
|
@ -40,7 +40,7 @@ export default function Component({ service }) {
|
||||
/>
|
||||
<Block
|
||||
label="frigate.uptime"
|
||||
value={t("common.uptime", {
|
||||
value={t("common.duration", {
|
||||
value: data.uptime,
|
||||
})}
|
||||
/>
|
||||
|
@ -44,7 +44,7 @@ export default function Component({ service }) {
|
||||
return (
|
||||
<Container service={service}>
|
||||
<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.maxUp" value={t("common.byterate", { value: fritzboxData.maxUp / 8, 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 disabled = infoData.filter((c) => c.enabled === 0).length;
|
||||
const enabled = infoData.filter((c) => !!c.enabled).length;
|
||||
const disabled = infoData.filter((c) => !c.enabled).length;
|
||||
const total = infoData.length;
|
||||
|
||||
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
|
||||
}
|
||||
} 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];
|
||||
}
|
||||
@ -50,19 +50,18 @@ export default async function npmProxyHandler(req, res) {
|
||||
const loginUrl = `${widget.url}/api/tokens`;
|
||||
|
||||
let status;
|
||||
let contentType;
|
||||
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(`HTTTP ${status} logging into npm api: ${token}`);
|
||||
logger.debug(`HTTP ${status} logging into npm api: ${token}`);
|
||||
return res.status(status).send(token);
|
||||
}
|
||||
}
|
||||
|
||||
[status, contentType, data] = await httpProxy(url, {
|
||||
[status, , data] = await httpProxy(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -71,17 +70,17 @@ export default async function npmProxyHandler(req, res) {
|
||||
});
|
||||
|
||||
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}`);
|
||||
[status, token] = await login(loginUrl, widget.username, widget.password, service);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[status, contentType, data] = await httpProxy(url, {
|
||||
[status, , data] = await httpProxy(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
|
@ -138,7 +138,7 @@ export default async function omadaProxyHandler(req, res) {
|
||||
const sitesResponseData = JSON.parse(data);
|
||||
|
||||
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
|
||||
.status(status)
|
||||
.json({ error: { message: "Error getting sites list", url, data: sitesResponseData } });
|
||||
|
@ -20,7 +20,7 @@ export default function Component({ service }) {
|
||||
|
||||
return (
|
||||
<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} />
|
||||
</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 = {
|
||||
body: params
|
||||
? Object.keys(params)
|
||||
.map((prop) => `${prop}=${params[prop]}`)
|
||||
.map((prop) => `${prop}=${encodeURIComponent(params[prop])}`)
|
||||
.join("&")
|
||||
: `session=${sessionId}`,
|
||||
method: "POST",
|
||||
|
@ -12,7 +12,10 @@ const widget = {
|
||||
wanted: jsonArrayFilter(data, (item) => item.monitored && !item.hasFile && item.isAvailable).length,
|
||||
have: jsonArrayFilter(data, (item) => 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": {
|
||||
|
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.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.playDuration" value={t("common.uptime", { value: stats.total_play_duration })} />
|
||||
<Block label="stash.playDuration" value={t("common.duration", { value: stats.total_play_duration })} />
|
||||
<Block
|
||||
label="stash.sceneSize"
|
||||
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
|
||||
|
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">
|
||||
{playing.map((session) => (
|
||||
<SessionEntry
|
||||
key={session.Id}
|
||||
key={session.session_key}
|
||||
session={session}
|
||||
enableUser={enableUser}
|
||||
showEpisodeNumber={showEpisodeNumber}
|
||||
|
@ -35,7 +35,7 @@ export default function Component({ service }) {
|
||||
<>
|
||||
<Container service={service}>
|
||||
<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 })} />
|
||||
</Container>
|
||||
{enablePools &&
|
||||
|
@ -1,4 +1,3 @@
|
||||
// import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
|
||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
||||
|
||||
const widget = {
|
||||
|
@ -58,7 +58,7 @@ export default function Component({ service }) {
|
||||
break;
|
||||
case 2:
|
||||
status = t("uptimerobot.up");
|
||||
uptime = t("common.uptime", { value: monitor.logs[0].duration });
|
||||
uptime = t("common.duration", { value: monitor.logs[0].duration });
|
||||
logIndex = 1;
|
||||
break;
|
||||
case 8:
|
||||
@ -73,7 +73,7 @@ export default function Component({ service }) {
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
|
@ -1,10 +1,12 @@
|
||||
import adguard from "./adguard/widget";
|
||||
import argocd from "./argocd/widget";
|
||||
import atsumeru from "./atsumeru/widget";
|
||||
import audiobookshelf from "./audiobookshelf/widget";
|
||||
import authentik from "./authentik/widget";
|
||||
import autobrr from "./autobrr/widget";
|
||||
import azuredevops from "./azuredevops/widget";
|
||||
import bazarr from "./bazarr/widget";
|
||||
import beszel from "./beszel/widget";
|
||||
import caddy from "./caddy/widget";
|
||||
import calendar from "./calendar/widget";
|
||||
import calibreweb from "./calibreweb/widget";
|
||||
@ -30,11 +32,13 @@ import gamedig from "./gamedig/widget";
|
||||
import gatus from "./gatus/widget";
|
||||
import ghostfolio from "./ghostfolio/widget";
|
||||
import gitea from "./gitea/widget";
|
||||
import gitlab from "./gitlab/widget";
|
||||
import glances from "./glances/widget";
|
||||
import gluetun from "./gluetun/widget";
|
||||
import gotify from "./gotify/widget";
|
||||
import grafana from "./grafana/widget";
|
||||
import hdhomerun from "./hdhomerun/widget";
|
||||
import headscale from "./headscale/widget";
|
||||
import homeassistant from "./homeassistant/widget";
|
||||
import homebox from "./homebox/widget";
|
||||
import homebridge from "./homebridge/widget";
|
||||
@ -85,6 +89,7 @@ import plantit from "./plantit/widget";
|
||||
import plex from "./plex/widget";
|
||||
import portainer from "./portainer/widget";
|
||||
import prometheus from "./prometheus/widget";
|
||||
import prometheusmetric from "./prometheusmetric/widget";
|
||||
import prowlarr from "./prowlarr/widget";
|
||||
import proxmox from "./proxmox/widget";
|
||||
import pterodactyl from "./pterodactyl/widget";
|
||||
@ -98,10 +103,12 @@ import sabnzbd from "./sabnzbd/widget";
|
||||
import scrutiny from "./scrutiny/widget";
|
||||
import sonarr from "./sonarr/widget";
|
||||
import speedtest from "./speedtest/widget";
|
||||
import spoolman from "./spoolman/widget";
|
||||
import stash from "./stash/widget";
|
||||
import stocks from "./stocks/widget";
|
||||
import strelaysrv from "./strelaysrv/widget";
|
||||
import swagdashboard from "./swagdashboard/widget";
|
||||
import suwayomi from "./suwayomi/widget";
|
||||
import tailscale from "./tailscale/widget";
|
||||
import tandoor from "./tandoor/widget";
|
||||
import tautulli from "./tautulli/widget";
|
||||
@ -126,12 +133,14 @@ import zabbix from "./zabbix/widget";
|
||||
|
||||
const widgets = {
|
||||
adguard,
|
||||
argocd,
|
||||
atsumeru,
|
||||
audiobookshelf,
|
||||
authentik,
|
||||
autobrr,
|
||||
azuredevops,
|
||||
bazarr,
|
||||
beszel,
|
||||
caddy,
|
||||
calibreweb,
|
||||
changedetectionio,
|
||||
@ -156,11 +165,13 @@ const widgets = {
|
||||
gatus,
|
||||
ghostfolio,
|
||||
gitea,
|
||||
gitlab,
|
||||
glances,
|
||||
gluetun,
|
||||
gotify,
|
||||
grafana,
|
||||
hdhomerun,
|
||||
headscale,
|
||||
homeassistant,
|
||||
homebox,
|
||||
homebridge,
|
||||
@ -214,6 +225,7 @@ const widgets = {
|
||||
plex,
|
||||
portainer,
|
||||
prometheus,
|
||||
prometheusmetric,
|
||||
prowlarr,
|
||||
proxmox,
|
||||
pterodactyl,
|
||||
@ -228,10 +240,12 @@ const widgets = {
|
||||
scrutiny,
|
||||
sonarr,
|
||||
speedtest,
|
||||
spoolman,
|
||||
stash,
|
||||
stocks,
|
||||
strelaysrv,
|
||||
swagdashboard,
|
||||
suwayomi,
|
||||
tailscale,
|
||||
tandoor,
|
||||
tautulli,
|
||||
|
Loading…
x
Reference in New Issue
Block a user