Merge branch 'dev'

main
shamoon 3 hours ago
commit 1656f02418

@ -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

@ -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.

@ -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.

@ -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
```

@ -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
```

@ -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)

@ -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
```

@ -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
```

@ -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

@ -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"

@ -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.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-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.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-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&current_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);

@ -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>
);
}

@ -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>

@ -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>
);
}

@ -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" });
}

@ -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 })} />

@ -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>
);
}

@ -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;

@ -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>
);
}

@ -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>
);

@ -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>
);
}

@ -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": {

@ -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>
);
}

@ -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

@ -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>
);
}

@ -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);
}

@ -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…
Cancel
Save