Compare commits

..

19 Commits
v1.12.0 ... dev

Author SHA1 Message Date
dependabot[bot]
f0f5c3c15c Chore(deps-dev): Bump @tailwindcss/forms from 0.5.10 to 0.5.11 (#6500)
Some checks are pending
Docker CI / Docker Build & Push (push) Waiting to run
Lint / Linting Checks (push) Waiting to run
Release Drafter / Update Release Draft (push) Waiting to run
Release Drafter / Auto Label PR (push) Waiting to run
Tests / vitest (1) (push) Waiting to run
Tests / vitest (2) (push) Waiting to run
Tests / vitest (3) (push) Waiting to run
Tests / vitest (4) (push) Waiting to run
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-01 22:02:55 +00:00
dependabot[bot]
2ed1da4411 Chore(deps): Bump i18next from 25.8.0 to 25.10.9 (#6501)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-01 21:58:21 +00:00
dependabot[bot]
e1023466b1 Chore(deps-dev): Bump @eslint/compat from 2.0.2 to 2.0.3 (#6498)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-01 21:53:25 +00:00
dependabot[bot]
9fe5ad62f1 Chore(deps-dev): Bump postcss from 8.5.6 to 8.5.8 (#6499)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-01 21:47:23 +00:00
dependabot[bot]
173883000f Chore(deps): Bump dockerode from 4.0.7 to 4.0.10 (#6497)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-01 14:42:40 -07:00
shamoon
d6e7e7e790 1.12.3
Some checks are pending
Docker CI / Docker Build & Push (push) Waiting to run
Docs / Test Build Docs (push) Waiting to run
Docs / Build & Deploy Docs (push) Waiting to run
Lint / Linting Checks (push) Waiting to run
Tests / vitest (1) (push) Waiting to run
Tests / vitest (2) (push) Waiting to run
Tests / vitest (3) (push) Waiting to run
Tests / vitest (4) (push) Waiting to run
2026-04-01 08:02:58 -07:00
shamoon
24cb274e03 Fix glances regex 2026-04-01 08:02:03 -07:00
shamoon
af852e748a Normalize widget version in URLs 2026-04-01 08:00:20 -07:00
shamoon
0ea5c3fb68 Bump package version to 1.12.2
Some checks failed
Crowdin Action / Crowdin Sync (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Docs / Test Build Docs (push) Has been cancelled
Docs / Build & Deploy Docs (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Repository Maintenance / Stale (push) Has been cancelled
Repository Maintenance / Lock Old Threads (push) Has been cancelled
Repository Maintenance / Close Answered Discussions (push) Has been cancelled
Repository Maintenance / Close Outdated Discussions (push) Has been cancelled
Repository Maintenance / Close Unsupported Feature Requests (push) Has been cancelled
2026-03-31 07:35:55 -07:00
shamoon
5ede96d6ce Merge branch 'dev' 2026-03-31 07:35:43 -07:00
github-actions[bot]
c50bc8601d New Crowdin translations by GitHub Action (#6470)
Some checks failed
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-03-31 07:34:56 -07:00
shamoon
463bb4e306 Chore: move lint checks to separate workflow (#6481)
Some checks failed
Docker CI / Docker Build & Push (push) Has been cancelled
Lint / Linting Checks (push) Has been cancelled
Release Drafter / Update Release Draft (push) Has been cancelled
Release Drafter / Auto Label PR (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
2026-03-29 13:18:45 -07:00
shamoon
4c3c4805c8 Security: pin GitHub Actions to specific SHAs (#6480) 2026-03-29 13:04:10 -07:00
Alex
a81ac47be9 Fix: fix compatibility with flood changes (#6477) 2026-03-29 13:48:43 +00:00
dependabot[bot]
36b909d4a4 Chore(deps): Bump brace-expansion from 1.1.12 to 1.1.13 in the npm_and_yarn group across 1 directory (#6478)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-29 06:15:45 -07:00
shamoon
a7fe80a399 Documentation: fix UniFi admonitions
Some checks failed
Docker CI / Linting Checks (push) Has been cancelled
Docker CI / Docker Build & Push (push) Has been cancelled
Docs / Linting Checks (push) Has been cancelled
Docs / Test Build Docs (push) Has been cancelled
Docs / Build & Deploy Docs (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (2) (push) Has been cancelled
Crowdin Action / Crowdin Sync (push) Has been cancelled
2026-03-28 21:05:28 -07:00
shamoon
0b61b6c1b8 Merge branch 'dev' 2026-03-27 20:23:47 -07:00
github-actions[bot]
7b552f5080 New Crowdin translations by GitHub Action (#6433)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-03-27 15:16:31 -07:00
Steven Harris
0f767d14bb Feature: UniFi Drive (UNAS) service widget (#6461)
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2026-03-27 15:10:18 -07:00
86 changed files with 956 additions and 341 deletions

View File

@@ -17,9 +17,9 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: crowdin action - name: crowdin action
uses: crowdin/github-action@v2 uses: crowdin/github-action@7ca9c452bfe9197d3bb7fa83a4d7e2b0c9ae835d # v2
with: with:
upload_translations: false upload_translations: false
download_translations: true download_translations: true

View File

@@ -17,44 +17,12 @@ env:
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
jobs: jobs:
pre-commit:
name: Linting Checks
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Check files
uses: pre-commit/action@v3.0.1
- name: Install pnpm
uses: pnpm/action-setup@v5
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Lint frontend
run: pnpm run lint
build: build:
name: Docker Build & Push name: Docker Build & Push
if: github.repository == 'gethomepage/homepage' if: github.repository == 'gethomepage/homepage'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: [ pre-commit ]
permissions: permissions:
contents: read contents: read
packages: write packages: write
@@ -62,11 +30,11 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Extract Docker metadata - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v6 uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6
with: with:
images: | images: |
${{ env.IMAGE_NAME }} ${{ env.IMAGE_NAME }}
@@ -84,7 +52,7 @@ jobs:
latest=auto latest=auto
- name: Next.js build cache - name: Next.js build cache
uses: actions/cache@v5 uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
with: with:
path: .next/cache path: .next/cache
key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx') }} key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx') }}
@@ -92,13 +60,13 @@ jobs:
nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@v5 uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with: with:
version: 10 version: 10
run_install: false run_install: false
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with: with:
node-version: 24 node-version: 24
cache: 'pnpm' cache: 'pnpm'
@@ -115,7 +83,7 @@ jobs:
- name: Log into registry ${{ env.REGISTRY }} - name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v4 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -123,20 +91,20 @@ jobs:
- name: Login to Docker Hub - name: Login to Docker Hub
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
uses: docker/login-action@v4 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@v4.0.0 uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Setup Docker buildx - name: Setup Docker buildx
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
- name: Build and push Docker image - name: Build and push Docker image
id: build-and-push id: build-and-push
uses: docker/build-push-action@v7 uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}

View File

@@ -14,32 +14,18 @@ permissions:
id-token: write id-token: write
jobs: jobs:
pre-commit:
name: Linting Checks
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install python
uses: actions/setup-python@v6
with:
python-version: 3.x
- name: Check files
uses: pre-commit/action@v3.0.1
test: test:
name: Test Build Docs name: Test Build Docs
if: github.repository == 'gethomepage/homepage' && github.event_name == 'pull_request' if: github.repository == 'gethomepage/homepage' && github.event_name == 'pull_request'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs:
- pre-commit
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-python@v6 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with: with:
python-version-file: ".python-version" python-version-file: ".python-version"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
- run: sudo apt-get install pngquant - run: sudo apt-get install pngquant
- name: Test Docs Build - name: Test Docs Build
run: uv run --frozen zensical build --clean run: uv run --frozen zensical build --clean
@@ -50,21 +36,19 @@ jobs:
environment: environment:
name: github-pages name: github-pages
url: ${{ steps.deployment.outputs.page_url }} url: ${{ steps.deployment.outputs.page_url }}
needs:
- pre-commit
steps: steps:
- uses: actions/configure-pages@v5 - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-python@v6 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with: with:
python-version-file: ".python-version" python-version-file: ".python-version"
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v7 uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
- run: sudo apt-get install pngquant - run: sudo apt-get install pngquant
- name: Build Docs - name: Build Docs
run: uv run --frozen zensical build --clean run: uv run --frozen zensical build --clean
- uses: actions/upload-pages-artifact@v4 - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
with: with:
path: site path: site
- uses: actions/deploy-pages@v4 - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
id: deployment id: deployment

41
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Lint
on:
pull_request:
push:
workflow_dispatch:
merge_group:
jobs:
lint:
name: Linting Checks
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Install python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: 3.x
- name: Check files
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
- name: Install pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with:
version: 10
run_install: false
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with:
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Lint frontend
run: pnpm run lint

View File

@@ -13,6 +13,6 @@ jobs:
anti-slop: anti-slop:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: peakoss/anti-slop@v0 - uses: peakoss/anti-slop@a5a4b2440c9de6f65b64f0718a0136a1fdb04f6f # v0
with: with:
max-failures: 4 max-failures: 4

View File

@@ -15,4 +15,4 @@ jobs:
action: action:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/reaction-comments@v4 - uses: dessant/reaction-comments@e86d247c12bd5c043eec379a1a4453f20cadf913 # v4

View File

@@ -26,14 +26,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- if: github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' - if: github.event_name == 'workflow_dispatch' && github.event.inputs.version != ''
uses: release-drafter/release-drafter@v7 uses: release-drafter/release-drafter@a6acf82562eee06318b77ab8cb0b11ed81c677a7 # v7
with: with:
config-name: release-drafter.yml config-name: release-drafter.yml
version: ${{ github.event.inputs.version }} version: ${{ github.event.inputs.version }}
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- if: github.event_name != 'workflow_dispatch' || github.event.inputs.version == '' - if: github.event_name != 'workflow_dispatch' || github.event.inputs.version == ''
uses: release-drafter/release-drafter@v7 uses: release-drafter/release-drafter@a6acf82562eee06318b77ab8cb0b11ed81c677a7 # v7
with: with:
config-name: release-drafter.yml config-name: release-drafter.yml
env: env:

View File

@@ -18,7 +18,7 @@ jobs:
name: 'Stale' name: 'Stale'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v10 - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10
with: with:
days-before-stale: 7 days-before-stale: 7
days-before-close: 14 days-before-close: 14
@@ -32,7 +32,7 @@ jobs:
name: 'Lock Old Threads' name: 'Lock Old Threads'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v6 - uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6
with: with:
issue-inactive-days: '30' issue-inactive-days: '30'
pr-inactive-days: '30' pr-inactive-days: '30'
@@ -57,7 +57,7 @@ jobs:
name: 'Close Answered Discussions' name: 'Close Answered Discussions'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with: with:
script: | script: |
function sleep(ms) { function sleep(ms) {
@@ -113,7 +113,7 @@ jobs:
name: 'Close Outdated Discussions' name: 'Close Outdated Discussions'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with: with:
script: | script: |
function sleep(ms) { function sleep(ms) {
@@ -204,7 +204,7 @@ jobs:
name: 'Close Unsupported Feature Requests' name: 'Close Unsupported Feature Requests'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/github-script@v8 - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
with: with:
script: | script: |
function sleep(ms) { function sleep(ms) {

View File

@@ -13,13 +13,13 @@ jobs:
matrix: matrix:
shard: [1, 2, 3, 4] shard: [1, 2, 3, 4]
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@v5 - uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
with: with:
version: 9 version: 9
- uses: actions/setup-node@v6 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
with: with:
node-version: 20 node-version: 20
cache: pnpm cache: pnpm
@@ -28,7 +28,7 @@ jobs:
# Run Vitest directly so `--shard` is parsed as an option # Run Vitest directly so `--shard` is parsed as an option
- run: pnpm -s exec vitest run --coverage --shard ${{ matrix.shard }}/4 --pool forks - run: pnpm -s exec vitest run --coverage --shard ${{ matrix.shard }}/4 --pool forks
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5 uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info files: ./coverage/lcov.info

View File

@@ -13,7 +13,7 @@ You can display general connectivity status from your Unifi (Network) Controller
An optional 'site' parameter can be supplied, if it is not the widget will use the default site for the controller. An optional 'site' parameter can be supplied, if it is not the widget will use the default site for the controller.
!!! hint !!! tip
If you enter e.g. incorrect credentials and receive an "API Error", you may need to recreate the container to clear the cache. If you enter e.g. incorrect credentials and receive an "API Error", you may need to recreate the container to clear the cache.

View File

@@ -17,7 +17,7 @@ An optional 'site' parameter can be supplied, if it is not the widget will use t
Allowed fields: `["uptime", "wan", "lan", "lan_users", "lan_devices", "wlan", "wlan_users", "wlan_devices"]` (maximum of four). Fields unsupported by the unifi device will not be shown. Allowed fields: `["uptime", "wan", "lan", "lan_users", "lan_devices", "wlan", "wlan_users", "wlan_devices"]` (maximum of four). Fields unsupported by the unifi device will not be shown.
!!! hint !!! tip
If you enter e.g. incorrect credentials and receive an "API Error", you may need to recreate the container or restart the service to clear the cache. If you enter e.g. incorrect credentials and receive an "API Error", you may need to recreate the container or restart the service to clear the cache.

View File

@@ -0,0 +1,24 @@
---
title: UniFi Drive
description: UniFi Drive Widget Configuration
---
Learn more about [UniFi Drive](https://ui.com/integrations/network-storage).
## Configuration
Displays storage statistics from your UniFi Network Attached Storage (UNAS) device. Requires a local UniFi account with at least read privileges.
Allowed fields: `["total", "used", "available", "status"]`
```yaml
widget:
type: unifi_drive
url: https://unifi.host.or.ip
username: your_username
password: your_password
```
!!! tip
If you enter incorrect credentials and receive an "API Error", you may need to recreate the container or restart the service to clear the cache.

View File

@@ -171,6 +171,7 @@ nav:
- widgets/services/truenas.md - widgets/services/truenas.md
- widgets/services/tubearchivist.md - widgets/services/tubearchivist.md
- widgets/services/unifi-controller.md - widgets/services/unifi-controller.md
- widgets/services/unifi-drive.md
- widgets/services/unmanic.md - widgets/services/unmanic.md
- widgets/services/unraid.md - widgets/services/unraid.md
- widgets/services/uptime-kuma.md - widgets/services/uptime-kuma.md

View File

@@ -1,6 +1,6 @@
{ {
"name": "homepage", "name": "homepage",
"version": "1.12.0", "version": "1.12.3",
"private": true, "private": true,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
@@ -18,10 +18,10 @@
"@kubernetes/client-node": "^1.0.0", "@kubernetes/client-node": "^1.0.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"compare-versions": "^6.1.1", "compare-versions": "^6.1.1",
"dockerode": "^4.0.7", "dockerode": "^4.0.10",
"follow-redirects": "^1.15.11", "follow-redirects": "^1.15.11",
"gamedig": "^5.3.2", "gamedig": "^5.3.2",
"i18next": "^25.8.0", "i18next": "^25.10.9",
"ical.js": "^2.2.1", "ical.js": "^2.2.1",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.1",
"json-rpc-2.0": "^1.7.0", "json-rpc-2.0": "^1.7.0",
@@ -47,10 +47,10 @@
"xml-js": "^1.6.11" "xml-js": "^1.6.11"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^2.0.2", "@eslint/compat": "^2.0.3",
"@eslint/eslintrc": "^3.3.3", "@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2", "@eslint/js": "^9.39.2",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"@testing-library/jest-dom": "^6.8.0", "@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
@@ -64,7 +64,7 @@
"eslint-plugin-react": "^7.37.4", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"jsdom": "^28.1.0", "jsdom": "^28.1.0",
"postcss": "^8.5.6", "postcss": "^8.5.8",
"prettier": "^3.8.1", "prettier": "^3.8.1",
"prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-organize-imports": "^4.3.0",
"tailwind-scrollbar": "^4.0.2", "tailwind-scrollbar": "^4.0.2",

228
pnpm-lock.yaml generated
View File

@@ -21,8 +21,8 @@ importers:
specifier: ^6.1.1 specifier: ^6.1.1
version: 6.1.1 version: 6.1.1
dockerode: dockerode:
specifier: ^4.0.7 specifier: ^4.0.10
version: 4.0.7 version: 4.0.10
follow-redirects: follow-redirects:
specifier: ^1.15.11 specifier: ^1.15.11
version: 1.15.11 version: 1.15.11
@@ -30,8 +30,8 @@ importers:
specifier: ^5.3.2 specifier: ^5.3.2
version: 5.3.2 version: 5.3.2
i18next: i18next:
specifier: ^25.8.0 specifier: ^25.10.9
version: 25.8.0(typescript@5.7.3) version: 25.10.9(typescript@5.7.3)
ical.js: ical.js:
specifier: ^2.2.1 specifier: ^2.2.1
version: 2.2.1 version: 2.2.1
@@ -55,7 +55,7 @@ importers:
version: 16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next-i18next: next-i18next:
specifier: ^15.4.3 specifier: ^15.4.3
version: 15.4.3(@types/react@19.0.10)(i18next@25.8.0(typescript@5.7.3))(next@16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-i18next@15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3))(react@19.2.4) version: 15.4.3(@types/react@19.0.10)(i18next@25.10.9(typescript@5.7.3))(next@16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-i18next@15.5.3(i18next@25.10.9(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3))(react@19.2.4)
ping: ping:
specifier: ^0.4.4 specifier: ^0.4.4
version: 0.4.4 version: 0.4.4
@@ -73,7 +73,7 @@ importers:
version: 19.2.4(react@19.2.4) version: 19.2.4(react@19.2.4)
react-i18next: react-i18next:
specifier: ^15.5.3 specifier: ^15.5.3
version: 15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3) version: 15.5.3(i18next@25.10.9(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
react-icons: react-icons:
specifier: ^5.6.0 specifier: ^5.6.0
version: 5.6.0(react@19.2.4) version: 5.6.0(react@19.2.4)
@@ -103,8 +103,8 @@ importers:
version: 1.6.11 version: 1.6.11
devDependencies: devDependencies:
'@eslint/compat': '@eslint/compat':
specifier: ^2.0.2 specifier: ^2.0.3
version: 2.0.2(eslint@9.25.1(jiti@2.6.1)) version: 2.0.3(eslint@9.25.1(jiti@2.6.1))
'@eslint/eslintrc': '@eslint/eslintrc':
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.3.3 version: 3.3.3
@@ -112,8 +112,8 @@ importers:
specifier: ^9.39.2 specifier: ^9.39.2
version: 9.39.2 version: 9.39.2
'@tailwindcss/forms': '@tailwindcss/forms':
specifier: ^0.5.10 specifier: ^0.5.11
version: 0.5.10(tailwindcss@4.1.18) version: 0.5.11(tailwindcss@4.1.18)
'@tailwindcss/postcss': '@tailwindcss/postcss':
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18 version: 4.1.18
@@ -125,7 +125,7 @@ importers:
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react@19.0.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 16.3.2(@testing-library/dom@10.4.1)(@types/react@19.0.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@vitest/coverage-v8': '@vitest/coverage-v8':
specifier: ^3.2.4 specifier: ^3.2.4
version: 3.2.4(vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)) version: 3.2.4(vitest@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2))
eslint: eslint:
specifier: ^9.25.1 specifier: ^9.25.1
version: 9.25.1(jiti@2.6.1) version: 9.25.1(jiti@2.6.1)
@@ -154,8 +154,8 @@ importers:
specifier: ^28.1.0 specifier: ^28.1.0
version: 28.1.0 version: 28.1.0
postcss: postcss:
specifier: ^8.5.6 specifier: ^8.5.8
version: 8.5.6 version: 8.5.8
prettier: prettier:
specifier: ^3.8.1 specifier: ^3.8.1
version: 3.8.1 version: 3.8.1
@@ -173,7 +173,7 @@ importers:
version: 5.7.3 version: 5.7.3
vitest: vitest:
specifier: ^3.2.4 specifier: ^3.2.4
version: 3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2) version: 3.2.4(@types/node@25.5.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)
optionalDependencies: optionalDependencies:
osx-temperature-sensor: osx-temperature-sensor:
specifier: ^1.0.8 specifier: ^1.0.8
@@ -462,8 +462,8 @@ packages:
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/compat@2.0.2': '@eslint/compat@2.0.3':
resolution: {integrity: sha512-pR1DoD0h3HfF675QZx0xsyrsU8q70Z/plx7880NOhS02NuWLgBCOMDL787nUeQ7EWLkxv3bPQJaarjcPQb2Dwg==} resolution: {integrity: sha512-SjIJhGigp8hmd1YGIBwh7Ovri7Kisl42GYFjrOyHhtfYGGoLW6teYi/5p8W50KSsawUPpuLOSmsq1bD0NGQLBw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
peerDependencies: peerDependencies:
eslint: ^8.40 || 9 || 10 eslint: ^8.40 || 9 || 10
@@ -483,8 +483,8 @@ packages:
resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/core@1.1.0': '@eslint/core@1.1.1':
resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==} resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24} engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/eslintrc@3.3.3': '@eslint/eslintrc@3.3.3':
@@ -537,8 +537,8 @@ packages:
'@floating-ui/utils@0.2.10': '@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@grpc/grpc-js@1.13.4': '@grpc/grpc-js@1.14.3':
resolution: {integrity: sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==} resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==}
engines: {node: '>=12.10.0'} engines: {node: '>=12.10.0'}
'@grpc/proto-loader@0.7.15': '@grpc/proto-loader@0.7.15':
@@ -546,6 +546,11 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
hasBin: true hasBin: true
'@grpc/proto-loader@0.8.0':
resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==}
engines: {node: '>=6'}
hasBin: true
'@headlessui/react@2.2.9': '@headlessui/react@2.2.9':
resolution: {integrity: sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==} resolution: {integrity: sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -1069,8 +1074,8 @@ packages:
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
'@tailwindcss/forms@0.5.10': '@tailwindcss/forms@0.5.11':
resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==} resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==}
peerDependencies: peerDependencies:
tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1' tailwindcss: '>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1'
@@ -1262,8 +1267,8 @@ packages:
'@types/node@22.13.4': '@types/node@22.13.4':
resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==} resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==}
'@types/node@24.1.0': '@types/node@25.5.0':
resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==} resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
'@types/prismjs@1.26.5': '@types/prismjs@1.26.5':
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
@@ -1593,11 +1598,11 @@ packages:
bl@4.1.0: bl@4.1.0:
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
brace-expansion@1.1.12: brace-expansion@1.1.13:
resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
brace-expansion@2.0.2: brace-expansion@2.0.3:
resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==}
braces@3.0.3: braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
@@ -1606,8 +1611,8 @@ packages:
buffer@5.7.1: buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
buildcheck@0.0.6: buildcheck@0.0.7:
resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==} resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
bytes@3.1.2: bytes@3.1.2:
@@ -1890,12 +1895,12 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
docker-modem@5.0.6: docker-modem@5.0.7:
resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==} resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==}
engines: {node: '>= 8.0'} engines: {node: '>= 8.0'}
dockerode@4.0.7: dockerode@4.0.10:
resolution: {integrity: sha512-R+rgrSRTRdU5mH14PZTCPZtW/zw3HDWNTS/1ZAQpL/5Upe/ye5K9WQkIysu4wBoiMwKynsz0a8qWuGsHgEvSAA==} resolution: {integrity: sha512-8L/P9JynLBiG7/coiA4FlQXegHltRqS0a+KqI44P1zgQh8QLHTg7FKOwhkBgSJwZTeHsq30WRoVFLuwkfK0YFg==}
engines: {node: '>= 8.0'} engines: {node: '>= 8.0'}
doctrine@2.1.0: doctrine@2.1.0:
@@ -2388,10 +2393,10 @@ packages:
i18next-fs-backend@2.6.1: i18next-fs-backend@2.6.1:
resolution: {integrity: sha512-eYWTX7QT7kJ0sZyCPK6x1q+R63zvNKv2D6UdbMf15A8vNb2ZLyw4NNNZxPFwXlIv/U+oUtg8SakW6ZgJZcoqHQ==} resolution: {integrity: sha512-eYWTX7QT7kJ0sZyCPK6x1q+R63zvNKv2D6UdbMf15A8vNb2ZLyw4NNNZxPFwXlIv/U+oUtg8SakW6ZgJZcoqHQ==}
i18next@25.8.0: i18next@25.10.9:
resolution: {integrity: sha512-urrg4HMFFMQZ2bbKRK7IZ8/CTE7D8H4JRlAwqA2ZwDRFfdd0K/4cdbNNLgfn9mo+I/h9wJu61qJzH7jCFAhUZQ==} resolution: {integrity: sha512-hQY9/bFoQKGlSKMlaCuLR8w1h5JjieqrsnZvEmj1Ja6Ec7fbyc4cTrCsY9mb9Sd8YQ/swsrKz1S9M8AcvVI70w==}
peerDependencies: peerDependencies:
typescript: ^5 typescript: ^5 || ^6
peerDependenciesMeta: peerDependenciesMeta:
typescript: typescript:
optional: true optional: true
@@ -2862,8 +2867,8 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nan@2.23.0: nan@2.26.2:
resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==} resolution: {integrity: sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==}
nanoid@3.3.11: nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
@@ -3043,8 +3048,8 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
postcss@8.5.6: postcss@8.5.8:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
engines: {node: ^10 || ^12 || >=14} engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1: prelude-ls@1.2.1:
@@ -3089,12 +3094,12 @@ packages:
prop-types@15.8.1: prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
protobufjs@7.5.3: protobufjs@7.5.4:
resolution: {integrity: sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==} resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
engines: {node: '>=12.0.0'} engines: {node: '>=12.0.0'}
pump@3.0.3: pump@3.0.4:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
punycode@2.3.1: punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
@@ -3357,8 +3362,8 @@ packages:
split-ca@1.0.1: split-ca@1.0.1:
resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==} resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==}
ssh2@1.16.0: ssh2@1.17.0:
resolution: {integrity: sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==} resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==}
engines: {node: '>=10.16.0'} engines: {node: '>=10.16.0'}
stable-hash@0.0.5: stable-hash@0.0.5:
@@ -3512,8 +3517,8 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'} engines: {node: '>=6'}
tar-fs@2.1.3: tar-fs@2.1.4:
resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==} resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
tar-stream@2.2.0: tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
@@ -3646,8 +3651,8 @@ packages:
undici-types@6.20.0: undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
undici-types@7.8.0: undici-types@7.18.2:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
undici@7.24.4: undici@7.24.4:
resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==} resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==}
@@ -4087,9 +4092,9 @@ snapshots:
'@eslint-community/regexpp@4.12.1': {} '@eslint-community/regexpp@4.12.1': {}
'@eslint/compat@2.0.2(eslint@9.25.1(jiti@2.6.1))': '@eslint/compat@2.0.3(eslint@9.25.1(jiti@2.6.1))':
dependencies: dependencies:
'@eslint/core': 1.1.0 '@eslint/core': 1.1.1
optionalDependencies: optionalDependencies:
eslint: 9.25.1(jiti@2.6.1) eslint: 9.25.1(jiti@2.6.1)
@@ -4107,7 +4112,7 @@ snapshots:
dependencies: dependencies:
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
'@eslint/core@1.1.0': '@eslint/core@1.1.1':
dependencies: dependencies:
'@types/json-schema': 7.0.15 '@types/json-schema': 7.0.15
@@ -4163,16 +4168,23 @@ snapshots:
'@floating-ui/utils@0.2.10': {} '@floating-ui/utils@0.2.10': {}
'@grpc/grpc-js@1.13.4': '@grpc/grpc-js@1.14.3':
dependencies: dependencies:
'@grpc/proto-loader': 0.7.15 '@grpc/proto-loader': 0.8.0
'@js-sdsl/ordered-map': 4.4.2 '@js-sdsl/ordered-map': 4.4.2
'@grpc/proto-loader@0.7.15': '@grpc/proto-loader@0.7.15':
dependencies: dependencies:
lodash.camelcase: 4.3.0 lodash.camelcase: 4.3.0
long: 5.3.2 long: 5.3.2
protobufjs: 7.5.3 protobufjs: 7.5.4
yargs: 17.7.2
'@grpc/proto-loader@0.8.0':
dependencies:
lodash.camelcase: 4.3.0
long: 5.3.2
protobufjs: 7.5.4
yargs: 17.7.2 yargs: 17.7.2
'@headlessui/react@2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': '@headlessui/react@2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
@@ -4606,7 +4618,7 @@ snapshots:
dependencies: dependencies:
defer-to-connect: 2.0.1 defer-to-connect: 2.0.1
'@tailwindcss/forms@0.5.10(tailwindcss@4.1.18)': '@tailwindcss/forms@0.5.11(tailwindcss@4.1.18)':
dependencies: dependencies:
mini-svg-data-uri: 1.4.4 mini-svg-data-uri: 1.4.4
tailwindcss: 4.1.18 tailwindcss: 4.1.18
@@ -4677,7 +4689,7 @@ snapshots:
'@alloc/quick-lru': 5.2.0 '@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.1.18 '@tailwindcss/node': 4.1.18
'@tailwindcss/oxide': 4.1.18 '@tailwindcss/oxide': 4.1.18
postcss: 8.5.6 postcss: 8.5.8
tailwindcss: 4.1.18 tailwindcss: 4.1.18
'@tanstack/react-virtual@3.13.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': '@tanstack/react-virtual@3.13.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
@@ -4781,9 +4793,9 @@ snapshots:
dependencies: dependencies:
undici-types: 6.20.0 undici-types: 6.20.0
'@types/node@24.1.0': '@types/node@25.5.0':
dependencies: dependencies:
undici-types: 7.8.0 undici-types: 7.18.2
'@types/prismjs@1.26.5': {} '@types/prismjs@1.26.5': {}
@@ -4859,7 +4871,7 @@ snapshots:
dependencies: dependencies:
'@typescript-eslint/types': 8.29.0 '@typescript-eslint/types': 8.29.0
'@typescript-eslint/visitor-keys': 8.29.0 '@typescript-eslint/visitor-keys': 8.29.0
debug: 4.4.3 debug: 4.4.1
fast-glob: 3.3.3 fast-glob: 3.3.3
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 9.0.5 minimatch: 9.0.5
@@ -4932,7 +4944,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.3.3': '@unrs/resolver-binding-win32-x64-msvc@1.3.3':
optional: true optional: true
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2))': '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2))':
dependencies: dependencies:
'@ampproject/remapping': 2.3.0 '@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2 '@bcoe/v8-coverage': 1.0.2
@@ -4947,7 +4959,7 @@ snapshots:
std-env: 3.10.0 std-env: 3.10.0
test-exclude: 7.0.1 test-exclude: 7.0.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vitest: 3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2) vitest: 3.2.4(@types/node@25.5.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -4959,13 +4971,13 @@ snapshots:
chai: 5.3.3 chai: 5.3.3
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2))': '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2))':
dependencies: dependencies:
'@vitest/spy': 3.2.4 '@vitest/spy': 3.2.4
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.21 magic-string: 0.30.21
optionalDependencies: optionalDependencies:
vite: 7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2) vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2)
'@vitest/pretty-format@3.2.4': '@vitest/pretty-format@3.2.4':
dependencies: dependencies:
@@ -5162,12 +5174,12 @@ snapshots:
inherits: 2.0.4 inherits: 2.0.4
readable-stream: 3.6.2 readable-stream: 3.6.2
brace-expansion@1.1.12: brace-expansion@1.1.13:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
concat-map: 0.0.1 concat-map: 0.0.1
brace-expansion@2.0.2: brace-expansion@2.0.3:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -5180,7 +5192,7 @@ snapshots:
base64-js: 1.5.1 base64-js: 1.5.1
ieee754: 1.2.1 ieee754: 1.2.1
buildcheck@0.0.6: buildcheck@0.0.7:
optional: true optional: true
bytes@3.1.2: {} bytes@3.1.2: {}
@@ -5293,8 +5305,8 @@ snapshots:
cpu-features@0.0.10: cpu-features@0.0.10:
dependencies: dependencies:
buildcheck: 0.0.6 buildcheck: 0.0.7
nan: 2.23.0 nan: 2.26.2
optional: true optional: true
cross-spawn@7.0.6: cross-spawn@7.0.6:
@@ -5434,23 +5446,23 @@ snapshots:
detect-libc@2.1.2: {} detect-libc@2.1.2: {}
docker-modem@5.0.6: docker-modem@5.0.7:
dependencies: dependencies:
debug: 4.4.1 debug: 4.4.3
readable-stream: 3.6.2 readable-stream: 3.6.2
split-ca: 1.0.1 split-ca: 1.0.1
ssh2: 1.16.0 ssh2: 1.17.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
dockerode@4.0.7: dockerode@4.0.10:
dependencies: dependencies:
'@balena/dockerignore': 1.0.2 '@balena/dockerignore': 1.0.2
'@grpc/grpc-js': 1.13.4 '@grpc/grpc-js': 1.14.3
'@grpc/proto-loader': 0.7.15 '@grpc/proto-loader': 0.7.15
docker-modem: 5.0.6 docker-modem: 5.0.7
protobufjs: 7.5.3 protobufjs: 7.5.4
tar-fs: 2.1.3 tar-fs: 2.1.4
uuid: 10.0.0 uuid: 10.0.0
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -6182,9 +6194,9 @@ snapshots:
i18next-fs-backend@2.6.1: {} i18next-fs-backend@2.6.1: {}
i18next@25.8.0(typescript@5.7.3): i18next@25.10.9(typescript@5.7.3):
dependencies: dependencies:
'@babel/runtime': 7.28.6 '@babel/runtime': 7.29.2
optionalDependencies: optionalDependencies:
typescript: 5.7.3 typescript: 5.7.3
@@ -6598,11 +6610,11 @@ snapshots:
minimatch@3.1.2: minimatch@3.1.2:
dependencies: dependencies:
brace-expansion: 1.1.12 brace-expansion: 1.1.13
minimatch@9.0.5: minimatch@9.0.5:
dependencies: dependencies:
brace-expansion: 2.0.2 brace-expansion: 2.0.3
minimist@1.2.8: {} minimist@1.2.8: {}
@@ -6618,7 +6630,7 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
nan@2.23.0: nan@2.26.2:
optional: true optional: true
nanoid@3.3.11: {} nanoid@3.3.11: {}
@@ -6627,17 +6639,17 @@ snapshots:
net@1.0.2: {} net@1.0.2: {}
next-i18next@15.4.3(@types/react@19.0.10)(i18next@25.8.0(typescript@5.7.3))(next@16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-i18next@15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3))(react@19.2.4): next-i18next@15.4.3(@types/react@19.0.10)(i18next@25.10.9(typescript@5.7.3))(next@16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-i18next@15.5.3(i18next@25.10.9(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3))(react@19.2.4):
dependencies: dependencies:
'@babel/runtime': 7.28.6 '@babel/runtime': 7.28.6
'@types/hoist-non-react-statics': 3.3.7(@types/react@19.0.10) '@types/hoist-non-react-statics': 3.3.7(@types/react@19.0.10)
core-js: 3.48.0 core-js: 3.48.0
hoist-non-react-statics: 3.3.2 hoist-non-react-statics: 3.3.2
i18next: 25.8.0(typescript@5.7.3) i18next: 25.10.9(typescript@5.7.3)
i18next-fs-backend: 2.6.1 i18next-fs-backend: 2.6.1
next: 16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next: 16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react: 19.2.4 react: 19.2.4
react-i18next: 15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3) react-i18next: 15.5.3(i18next@25.10.9(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/react' - '@types/react'
@@ -6796,7 +6808,7 @@ snapshots:
picocolors: 1.1.1 picocolors: 1.1.1
source-map-js: 1.2.1 source-map-js: 1.2.1
postcss@8.5.6: postcss@8.5.8:
dependencies: dependencies:
nanoid: 3.3.11 nanoid: 3.3.11
picocolors: 1.1.1 picocolors: 1.1.1
@@ -6837,7 +6849,7 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
react-is: 16.13.1 react-is: 16.13.1
protobufjs@7.5.3: protobufjs@7.5.4:
dependencies: dependencies:
'@protobufjs/aspromise': 1.1.2 '@protobufjs/aspromise': 1.1.2
'@protobufjs/base64': 1.1.2 '@protobufjs/base64': 1.1.2
@@ -6849,10 +6861,10 @@ snapshots:
'@protobufjs/path': 1.1.2 '@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0 '@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0 '@protobufjs/utf8': 1.1.0
'@types/node': 24.1.0 '@types/node': 25.5.0
long: 5.3.2 long: 5.3.2
pump@3.0.3: pump@3.0.4:
dependencies: dependencies:
end-of-stream: 1.4.5 end-of-stream: 1.4.5
once: 1.4.0 once: 1.4.0
@@ -6875,11 +6887,11 @@ snapshots:
react: 19.2.4 react: 19.2.4
scheduler: 0.27.0 scheduler: 0.27.0
react-i18next@15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3): react-i18next@15.5.3(i18next@25.10.9(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3):
dependencies: dependencies:
'@babel/runtime': 7.27.6 '@babel/runtime': 7.27.6
html-parse-stringify: 3.0.1 html-parse-stringify: 3.0.1
i18next: 25.8.0(typescript@5.7.3) i18next: 25.10.9(typescript@5.7.3)
react: 19.2.4 react: 19.2.4
optionalDependencies: optionalDependencies:
react-dom: 19.2.4(react@19.2.4) react-dom: 19.2.4(react@19.2.4)
@@ -7190,13 +7202,13 @@ snapshots:
split-ca@1.0.1: {} split-ca@1.0.1: {}
ssh2@1.16.0: ssh2@1.17.0:
dependencies: dependencies:
asn1: 0.2.6 asn1: 0.2.6
bcrypt-pbkdf: 1.0.2 bcrypt-pbkdf: 1.0.2
optionalDependencies: optionalDependencies:
cpu-features: 0.0.10 cpu-features: 0.0.10
nan: 2.23.0 nan: 2.26.2
stable-hash@0.0.5: {} stable-hash@0.0.5: {}
@@ -7356,11 +7368,11 @@ snapshots:
tapable@2.3.0: {} tapable@2.3.0: {}
tar-fs@2.1.3: tar-fs@2.1.4:
dependencies: dependencies:
chownr: 1.1.4 chownr: 1.1.4
mkdirp-classic: 0.5.3 mkdirp-classic: 0.5.3
pump: 3.0.3 pump: 3.0.4
tar-stream: 2.2.0 tar-stream: 2.2.0
tar-stream@2.2.0: tar-stream@2.2.0:
@@ -7507,7 +7519,7 @@ snapshots:
undici-types@6.20.0: {} undici-types@6.20.0: {}
undici-types@7.8.0: {} undici-types@7.18.2: {}
undici@7.24.4: {} undici@7.24.4: {}
@@ -7570,13 +7582,13 @@ snapshots:
d3-time: 3.1.0 d3-time: 3.1.0
d3-timer: 3.0.1 d3-timer: 3.0.1
vite-node@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2): vite-node@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2):
dependencies: dependencies:
cac: 6.7.14 cac: 6.7.14
debug: 4.4.1 debug: 4.4.1
es-module-lexer: 1.7.0 es-module-lexer: 1.7.0
pathe: 2.0.3 pathe: 2.0.3
vite: 7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2) vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
- jiti - jiti
@@ -7591,25 +7603,25 @@ snapshots:
- tsx - tsx
- yaml - yaml
vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2): vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2):
dependencies: dependencies:
esbuild: 0.27.2 esbuild: 0.27.2
fdir: 6.5.0(picomatch@4.0.4) fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4 picomatch: 4.0.4
postcss: 8.5.6 postcss: 8.5.8
rollup: 4.59.0 rollup: 4.59.0
tinyglobby: 0.2.15 tinyglobby: 0.2.15
optionalDependencies: optionalDependencies:
'@types/node': 24.1.0 '@types/node': 25.5.0
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.6.1 jiti: 2.6.1
lightningcss: 1.30.2 lightningcss: 1.30.2
vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2): vitest@3.2.4(@types/node@25.5.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2):
dependencies: dependencies:
'@types/chai': 5.2.3 '@types/chai': 5.2.3
'@vitest/expect': 3.2.4 '@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2)) '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2))
'@vitest/pretty-format': 3.2.4 '@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4 '@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4 '@vitest/snapshot': 3.2.4
@@ -7627,11 +7639,11 @@ snapshots:
tinyglobby: 0.2.15 tinyglobby: 0.2.15
tinypool: 1.1.1 tinypool: 1.1.1
tinyrainbow: 2.0.0 tinyrainbow: 2.0.0
vite: 7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2) vite: 7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2)
vite-node: 3.2.4(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2) vite-node: 3.2.4(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 24.1.0 '@types/node': 25.5.0
jsdom: 28.1.0 jsdom: 28.1.0
transitivePeerDependencies: transitivePeerDependencies:
- jiti - jiti

View File

@@ -66,6 +66,11 @@
"wait": "Wag asseblief", "wait": "Wag asseblief",
"empty_data": "Substelsel status onbekend" "empty_data": "Substelsel status onbekend"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "حالة النظام الفرعي غير معروفة" "empty_data": "حالة النظام الفرعي غير معروفة"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "استقبال", "rx": "استقبال",
"tx": "ارسال", "tx": "ارسال",

View File

@@ -66,6 +66,11 @@
"wait": "Моля изчакайте", "wait": "Моля изчакайте",
"empty_data": "Неизвестен статус на подсистема" "empty_data": "Неизвестен статус на подсистема"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "ПЧ", "rx": "ПЧ",
"tx": "ИЗ", "tx": "ИЗ",

View File

@@ -66,6 +66,11 @@
"wait": "Si us plau espera", "wait": "Si us plau espera",
"empty_data": "Estat del subsistema desconegut" "empty_data": "Estat del subsistema desconegut"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Rebut", "rx": "Rebut",
"tx": "Transmès", "tx": "Transmès",

View File

@@ -39,7 +39,7 @@
"placeholder": "Hledat…" "placeholder": "Hledat…"
}, },
"resources": { "resources": {
"cpu": "Zatížení procesoru", "cpu": "Využití procesoru",
"mem": "Využití paměti", "mem": "Využití paměti",
"total": "Celkem", "total": "Celkem",
"free": "Volné", "free": "Volné",
@@ -66,11 +66,16 @@
"wait": "Čekejte prosím", "wait": "Čekejte prosím",
"empty_data": "Stav podsystému neznámý" "empty_data": "Stav podsystému neznámý"
}, },
"unifi_drive": {
"healthy": "Zdravý",
"degraded": "Degradováno",
"no_data": "Nejsou k dispozici žádná data úložiště"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",
"mem": "Využití paměti", "mem": "Využití paměti",
"cpu": "Zatížení procesoru", "cpu": "Využití procesoru",
"running": "Běží", "running": "Běží",
"offline": "Offline", "offline": "Offline",
"error": "Chyba", "error": "Chyba",
@@ -232,7 +237,7 @@
"seed": "Seedované" "seed": "Seedované"
}, },
"qnap": { "qnap": {
"cpuUsage": "Zatížení procesoru", "cpuUsage": "Využití procesoru",
"memUsage": "Využití paměti", "memUsage": "Využití paměti",
"systemTempC": "Teplota systému", "systemTempC": "Teplota systému",
"poolUsage": "Využití fondu", "poolUsage": "Využití fondu",
@@ -445,12 +450,12 @@
}, },
"proxmox": { "proxmox": {
"mem": "Využití paměti", "mem": "Využití paměti",
"cpu": "Zatížení procesoru", "cpu": "Využití procesoru",
"lxc": "LXC", "lxc": "LXC",
"vms": "Virtuální Stroje" "vms": "Virtuální Stroje"
}, },
"glances": { "glances": {
"cpu": "Zatížení procesoru", "cpu": "Využití procesoru",
"load": "Zatížení", "load": "Zatížení",
"wait": "Čekejte prosím", "wait": "Čekejte prosím",
"temp": "TEPLOTA", "temp": "TEPLOTA",
@@ -635,7 +640,7 @@
"no_devices": "Žádná přijatá data zařízení" "no_devices": "Žádná přijatá data zařízení"
}, },
"mikrotik": { "mikrotik": {
"cpuLoad": "Zatížení procesoru", "cpuLoad": "Využití procesoru",
"memoryUsed": "Využití paměti", "memoryUsed": "Využití paměti",
"uptime": "Doba provozu", "uptime": "Doba provozu",
"numberOfLeases": "Pronájmy" "numberOfLeases": "Pronájmy"
@@ -686,7 +691,7 @@
"proxmoxbackupserver": { "proxmoxbackupserver": {
"datastore_usage": "Datové úložiště", "datastore_usage": "Datové úložiště",
"failed_tasks_24h": "Neúspěšné úlohy 24h", "failed_tasks_24h": "Neúspěšné úlohy 24h",
"cpu_usage": "Zatížení procesoru", "cpu_usage": "Využití procesoru",
"memory_usage": "Využití paměti" "memory_usage": "Využití paměti"
}, },
"immich": { "immich": {
@@ -750,7 +755,7 @@
"alertstriggered": "Spuštěné výstrahy" "alertstriggered": "Spuštěné výstrahy"
}, },
"nextcloud": { "nextcloud": {
"cpuload": "Zatížení procesoru", "cpuload": "Využití procesoru",
"memoryusage": "Využití paměti", "memoryusage": "Využití paměti",
"freespace": "Volný prostor", "freespace": "Volný prostor",
"activeusers": "Aktivní uživatelé", "activeusers": "Aktivní uživatelé",
@@ -873,7 +878,7 @@
}, },
"openwrt": { "openwrt": {
"uptime": "Doba provozu", "uptime": "Doba provozu",
"cpuLoad": "Prům. zatížení procesoru (5m)", "cpuLoad": "Prům. využití procesoru (5m)",
"up": "Běží", "up": "Běží",
"down": "Výpadek", "down": "Výpadek",
"bytesTx": "Přeneseno", "bytesTx": "Přeneseno",
@@ -1037,7 +1042,7 @@
"pending": "Čekající", "pending": "Čekající",
"status": "Stav", "status": "Stav",
"updated": "Aktualizováno", "updated": "Aktualizováno",
"cpu": "Zatížení procesoru", "cpu": "Využití procesoru",
"memory": "Využití paměti", "memory": "Využití paměti",
"disk": "Disk", "disk": "Disk",
"network": "Síť" "network": "Síť"
@@ -1133,7 +1138,7 @@
"NO_DATA_DISKS": "Žádné datové disky", "NO_DATA_DISKS": "Žádné datové disky",
"notifications": "Upozornění", "notifications": "Upozornění",
"status": "Stav", "status": "Stav",
"cpu": "Zatížení procesoru", "cpu": "Využití procesoru",
"memoryUsed": "Využití paměti", "memoryUsed": "Využití paměti",
"memoryAvailable": "Volná paměť", "memoryAvailable": "Volná paměť",
"arrayUsed": "Využito pole", "arrayUsed": "Využito pole",
@@ -1164,7 +1169,7 @@
"dockhand": { "dockhand": {
"running": "Běží", "running": "Běží",
"stopped": "Zastaveno", "stopped": "Zastaveno",
"cpu": "Zatížení procesoru", "cpu": "Využití procesoru",
"memory": "Využití paměti", "memory": "Využití paměti",
"images": "Obrazy", "images": "Obrazy",
"volumes": "Úložiště", "volumes": "Úložiště",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status ukendt" "empty_data": "Subsystem status ukendt"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Bitte warten", "wait": "Bitte warten",
"empty_data": "Subsystem-Status unbekannt" "empty_data": "Subsystem-Status unbekannt"
}, },
"unifi_drive": {
"healthy": "Gesund",
"degraded": "Beeinträchtigt",
"no_data": "Keine Speicherdaten verfügbar"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",
@@ -115,7 +120,7 @@
"movies": "Filme", "movies": "Filme",
"series": "Serien", "series": "Serien",
"episodes": "Episoden", "episodes": "Episoden",
"songs": "Songs" "songs": "Titel"
}, },
"esphome": { "esphome": {
"offline": "Offline", "offline": "Offline",
@@ -185,10 +190,10 @@
"plex_connection_error": "Prüfe Plex-Verbindung" "plex_connection_error": "Prüfe Plex-Verbindung"
}, },
"tracearr": { "tracearr": {
"no_active": "No Active Streams", "no_active": "Keine aktiven Streams",
"streams": "Streams", "streams": "Streams",
"transcodes": "Transcodes", "transcodes": "Transkodieren",
"directplay": "Direct Play", "directplay": "Direkte Wiedergabe",
"bitrate": "Bitrate" "bitrate": "Bitrate"
}, },
"omada": { "omada": {
@@ -290,12 +295,12 @@
"available": "Verfügbar" "available": "Verfügbar"
}, },
"seerr": { "seerr": {
"pending": "Pending", "pending": "Ausstehend",
"approved": "Approved", "approved": "Bestätigt",
"available": "Available", "available": "Verfügbar",
"completed": "Completed", "completed": "Abgeschlossen",
"processing": "Processing", "processing": "Wird verarbeitet",
"issues": "Open Issues" "issues": "Offene Probleme"
}, },
"netalertx": { "netalertx": {
"total": "Total", "total": "Total",
@@ -615,7 +620,7 @@
}, },
"pangolin": { "pangolin": {
"orgs": "Orgs", "orgs": "Orgs",
"sites": "Sites", "sites": "Seiten",
"resources": "Ressourcen", "resources": "Ressourcen",
"targets": "Ziele", "targets": "Ziele",
"traffic": "Traffic", "traffic": "Traffic",
@@ -719,7 +724,7 @@
"volumeAvailable": "Verfügbar" "volumeAvailable": "Verfügbar"
}, },
"dispatcharr": { "dispatcharr": {
"channels": "Channels", "channels": "Kanäle",
"streams": "Streams" "streams": "Streams"
}, },
"mylar": { "mylar": {
@@ -811,10 +816,10 @@
"series": "Serien" "series": "Serien"
}, },
"booklore": { "booklore": {
"libraries": "Libraries", "libraries": "Bibliotheken",
"books": "Bücher", "books": "Bücher",
"reading": "Reading", "reading": "Am Lesen",
"finished": "Finished" "finished": "Fertig"
}, },
"jdownloader": { "jdownloader": {
"downloadCount": "Warteschlange", "downloadCount": "Warteschlange",
@@ -1155,11 +1160,11 @@
"artists": "Künstler" "artists": "Künstler"
}, },
"arcane": { "arcane": {
"containers": "Containers", "containers": "Container",
"images": "Images", "images": "Images",
"image_updates": "Image Updates", "image_updates": "Image-Updates",
"images_unused": "Unused", "images_unused": "Ungenutzt",
"environment_required": "Environment ID Required" "environment_required": "Umgebungs-ID erforderlich"
}, },
"dockhand": { "dockhand": {
"running": "Wird ausgeführt", "running": "Wird ausgeführt",
@@ -1176,9 +1181,9 @@
"environment_not_found": "Umgebung nicht gefunden" "environment_not_found": "Umgebung nicht gefunden"
}, },
"sparkyfitness": { "sparkyfitness": {
"eaten": "Eaten", "eaten": "",
"burned": "Burned", "burned": "Verbrannt",
"remaining": "Remaining", "remaining": "Verbleibend",
"steps": "Steps" "steps": "Schritte"
} }
} }

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Άγνωστη κατάσταση υποσυστήματος" "empty_data": "Άγνωστη κατάσταση υποσυστήματος"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsistemostatuso nekonata" "empty_data": "Subsistemostatuso nekonata"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Espere, por favor", "wait": "Espere, por favor",
"empty_data": "Se desconoce el estado del subsistema" "empty_data": "Se desconoce el estado del subsistema"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Recibido", "rx": "Recibido",
"tx": "Transmitido", "tx": "Transmitido",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Veuillez patienter", "wait": "Veuillez patienter",
"empty_data": "Statut du sous-système inconnu" "empty_data": "Statut du sous-système inconnu"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Rx", "rx": "Rx",
"tx": "Tx", "tx": "Tx",

View File

@@ -66,6 +66,11 @@
"wait": "נא להמתין", "wait": "נא להמתין",
"empty_data": "מצב תת-מערכת לא ידוע" "empty_data": "מצב תת-מערכת לא ידוע"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Pričekaj", "wait": "Pričekaj",
"empty_data": "Stanje podsustava nepoznato" "empty_data": "Stanje podsustava nepoznato"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Kérjük várjon", "wait": "Kérjük várjon",
"empty_data": "Az alrendszer állapota ismeretlen" "empty_data": "Az alrendszer állapota ismeretlen"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Status subsistem tdk diketahui" "empty_data": "Status subsistem tdk diketahui"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Stato del sottosistema sconosciuto" "empty_data": "Stato del sottosistema sconosciuto"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "お待ちください", "wait": "お待ちください",
"empty_data": "サブシステムの状態は不明" "empty_data": "サブシステムの状態は不明"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "受信済み", "rx": "受信済み",
"tx": "送信済み", "tx": "送信済み",

View File

@@ -66,6 +66,11 @@
"wait": "잠시만 기다려주세요", "wait": "잠시만 기다려주세요",
"empty_data": "서브시스템 상태 알 수 없음" "empty_data": "서브시스템 상태 알 수 없음"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "수신", "rx": "수신",
"tx": "송신", "tx": "송신",
@@ -108,14 +113,14 @@
"songs": "음악" "songs": "음악"
}, },
"jellyfin": { "jellyfin": {
"playing": "Playing", "playing": "재생 중",
"transcoding": "Transcoding", "transcoding": "트랜스코딩 중",
"bitrate": "Bitrate", "bitrate": "비트레이트",
"no_active": "No Active Streams", "no_active": "활성 스트림 없음",
"movies": "Movies", "movies": "영상",
"series": "Series", "series": "시리즈",
"episodes": "Episodes", "episodes": "에피소드",
"songs": "Songs" "songs": "음악"
}, },
"esphome": { "esphome": {
"offline": "오프라인", "offline": "오프라인",
@@ -185,11 +190,11 @@
"plex_connection_error": "Plex 연결 확인" "plex_connection_error": "Plex 연결 확인"
}, },
"tracearr": { "tracearr": {
"no_active": "No Active Streams", "no_active": "활성 스트림 없음",
"streams": "Streams", "streams": "스트림",
"transcodes": "Transcodes", "transcodes": "트랜스코드",
"directplay": "Direct Play", "directplay": "다이렉트 플레이",
"bitrate": "Bitrate" "bitrate": "비트레이트"
}, },
"omada": { "omada": {
"connectedAp": "연결된 AP", "connectedAp": "연결된 AP",
@@ -290,12 +295,12 @@
"available": "이용 가능" "available": "이용 가능"
}, },
"seerr": { "seerr": {
"pending": "Pending", "pending": "대기 중",
"approved": "Approved", "approved": "승인됨",
"available": "Available", "available": "사용 가능",
"completed": "Completed", "completed": "완료됨",
"processing": "Processing", "processing": "처리 중",
"issues": "Open Issues" "issues": "열린 이슈"
}, },
"netalertx": { "netalertx": {
"total": "전체", "total": "전체",
@@ -546,7 +551,7 @@
"up": "업", "up": "업",
"pending": "대기 중", "pending": "대기 중",
"down": "다운", "down": "다운",
"ok": "Ok" "ok": "확인"
}, },
"healthchecks": { "healthchecks": {
"new": "신규", "new": "신규",
@@ -618,9 +623,9 @@
"sites": "Sites", "sites": "Sites",
"resources": "Resources", "resources": "Resources",
"targets": "Targets", "targets": "Targets",
"traffic": "Traffic", "traffic": "트래픽",
"in": "In", "in": "수신",
"out": "Out" "out": "송신"
}, },
"peanut": { "peanut": {
"battery_charge": "배터리 충전", "battery_charge": "배터리 충전",
@@ -719,8 +724,8 @@
"volumeAvailable": "사용 가능" "volumeAvailable": "사용 가능"
}, },
"dispatcharr": { "dispatcharr": {
"channels": "Channels", "channels": "채널",
"streams": "Streams" "streams": "스트림"
}, },
"mylar": { "mylar": {
"series": "시리즈", "series": "시리즈",
@@ -787,7 +792,7 @@
"gross_percent_today": "오늘", "gross_percent_today": "오늘",
"gross_percent_1y": "1년", "gross_percent_1y": "1년",
"gross_percent_max": "전체 기간", "gross_percent_max": "전체 기간",
"net_worth": "Net Worth" "net_worth": "순자산"
}, },
"audiobookshelf": { "audiobookshelf": {
"podcasts": "팟캐스트", "podcasts": "팟캐스트",
@@ -811,10 +816,10 @@
"series": "시리즈" "series": "시리즈"
}, },
"booklore": { "booklore": {
"libraries": "Libraries", "libraries": "라이브러리",
"books": "Books", "books": "",
"reading": "Reading", "reading": "읽는 중",
"finished": "Finished" "finished": "완료"
}, },
"jdownloader": { "jdownloader": {
"downloadCount": "대기열", "downloadCount": "대기열",
@@ -1150,30 +1155,30 @@
"bytes_added_30": "추가된 용량" "bytes_added_30": "추가된 용량"
}, },
"yourspotify": { "yourspotify": {
"songs": "Songs", "songs": "음악",
"time": "Time", "time": "시간",
"artists": "Artists" "artists": "아티스트"
}, },
"arcane": { "arcane": {
"containers": "Containers", "containers": "컨테이너",
"images": "Images", "images": "이미지",
"image_updates": "Image Updates", "image_updates": "이미지 업데이트",
"images_unused": "Unused", "images_unused": "미사용",
"environment_required": "Environment ID Required" "environment_required": "환경 ID 필요"
}, },
"dockhand": { "dockhand": {
"running": "Running", "running": "실행 중",
"stopped": "Stopped", "stopped": "정지됨",
"cpu": "CPU", "cpu": "CPU",
"memory": "Memory", "memory": "메모리",
"images": "Images", "images": "이미지",
"volumes": "Volumes", "volumes": "볼륨",
"events_today": "Events Today", "events_today": "오늘의 이벤트",
"pending_updates": "Pending Updates", "pending_updates": "대기 중인 업데이트",
"stacks": "Stacks", "stacks": "스택",
"paused": "Paused", "paused": "일시정지됨",
"total": "Total", "total": "전체",
"environment_not_found": "Environment Not Found" "environment_not_found": "환경 없음"
}, },
"sparkyfitness": { "sparkyfitness": {
"eaten": "Eaten", "eaten": "Eaten",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Status subsistem tak diketahui" "empty_data": "Status subsistem tak diketahui"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Even geduld", "wait": "Even geduld",
"empty_data": "Subsysteem status onbekend" "empty_data": "Subsysteem status onbekend"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Ukjent undersystemstatus" "empty_data": "Ukjent undersystemstatus"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Proszę czekać", "wait": "Proszę czekać",
"empty_data": "Status podsystemu nieznany" "empty_data": "Status podsystemu nieznany"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Rx", "rx": "Rx",
"tx": "Tx", "tx": "Tx",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Status de Subsistema Desconhecido" "empty_data": "Status de Subsistema Desconhecido"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Rx", "rx": "Rx",
"tx": "Tx", "tx": "Tx",

View File

@@ -14,7 +14,7 @@
"date": "{{value, date}}", "date": "{{value, date}}",
"relativeDate": "{{value, relativeDate}}", "relativeDate": "{{value, relativeDate}}",
"duration": "{{value, duration}}", "duration": "{{value, duration}}",
"months": "M", "months": "mo",
"days": "d", "days": "d",
"hours": "h", "hours": "h",
"minutes": "m", "minutes": "m",
@@ -66,9 +66,14 @@
"wait": "Por favor, aguarde", "wait": "Por favor, aguarde",
"empty_data": "Status do Subsistema desconhecido" "empty_data": "Status do Subsistema desconhecido"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Rx", "rx": "RX",
"tx": "Tx", "tx": "TX",
"mem": "MEM", "mem": "MEM",
"cpu": "CPU", "cpu": "CPU",
"running": "Executando", "running": "Executando",
@@ -101,21 +106,21 @@
"playing": "A reproduzir", "playing": "A reproduzir",
"transcoding": "Transcodificação", "transcoding": "Transcodificação",
"bitrate": "Taxa de bits", "bitrate": "Taxa de bits",
"no_active": "Sem Streams Ativos", "no_active": "Sem Transmissões Ativas",
"movies": "Filmes", "movies": "Filmes",
"series": "Séries", "series": "Séries",
"episodes": "Episódios", "episodes": "Episódios",
"songs": "Canções" "songs": "Canções"
}, },
"jellyfin": { "jellyfin": {
"playing": "Playing", "playing": "Jogando",
"transcoding": "Transcoding", "transcoding": "Transcoding",
"bitrate": "Bitrate", "bitrate": "Bitrate",
"no_active": "No Active Streams", "no_active": "No Active Streams",
"movies": "Movies", "movies": "Filmes",
"series": "Series", "series": "Séries",
"episodes": "Episodes", "episodes": "Episódios",
"songs": "Songs" "songs": "Músicas"
}, },
"esphome": { "esphome": {
"offline": "Offline", "offline": "Offline",
@@ -290,12 +295,12 @@
"available": "Disponível" "available": "Disponível"
}, },
"seerr": { "seerr": {
"pending": "Pending", "pending": "Pendente",
"approved": "Approved", "approved": "Aprovado",
"available": "Available", "available": "Disponível",
"completed": "Completed", "completed": "Concluído",
"processing": "Processing", "processing": "Processando",
"issues": "Open Issues" "issues": "Erros pendentes"
}, },
"netalertx": { "netalertx": {
"total": "Total", "total": "Total",
@@ -616,7 +621,7 @@
"pangolin": { "pangolin": {
"orgs": "Orgs", "orgs": "Orgs",
"sites": "Sites", "sites": "Sites",
"resources": "Resources", "resources": "Recursos",
"targets": "Targets", "targets": "Targets",
"traffic": "Traffic", "traffic": "Traffic",
"in": "In", "in": "In",
@@ -719,8 +724,8 @@
"volumeAvailable": "Disponível" "volumeAvailable": "Disponível"
}, },
"dispatcharr": { "dispatcharr": {
"channels": "Channels", "channels": "Canais",
"streams": "Streams" "streams": "Transmissões"
}, },
"mylar": { "mylar": {
"series": "Séries", "series": "Séries",
@@ -811,10 +816,10 @@
"series": "Séries" "series": "Séries"
}, },
"booklore": { "booklore": {
"libraries": "Libraries", "libraries": "Bibliotecas",
"books": "Books", "books": "Livros",
"reading": "Reading", "reading": "Lendo",
"finished": "Finished" "finished": "Finalizado"
}, },
"jdownloader": { "jdownloader": {
"downloadCount": "Fila de espera", "downloadCount": "Fila de espera",
@@ -1155,23 +1160,23 @@
"artists": "Artistas" "artists": "Artistas"
}, },
"arcane": { "arcane": {
"containers": "Containers", "containers": "Recipientes",
"images": "Images", "images": "Imagens",
"image_updates": "Image Updates", "image_updates": "Atualizações de Imagem",
"images_unused": "Unused", "images_unused": "Não utilizado",
"environment_required": "Environment ID Required" "environment_required": "Environment ID Required"
}, },
"dockhand": { "dockhand": {
"running": "Running", "running": "Executando",
"stopped": "Stopped", "stopped": "Stopped",
"cpu": "CPU", "cpu": "CPU",
"memory": "Memory", "memory": "Memória",
"images": "Images", "images": "Imagens",
"volumes": "Volumes", "volumes": "Quantidades",
"events_today": "Events Today", "events_today": "Eventos hoje",
"pending_updates": "Pending Updates", "pending_updates": "Atualizações pendentes",
"stacks": "Stacks", "stacks": "Pilhas",
"paused": "Paused", "paused": "Pausado",
"total": "Total", "total": "Total",
"environment_not_found": "Environment Not Found" "environment_not_found": "Environment Not Found"
}, },

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Starea subsistemului este necunoscut" "empty_data": "Starea subsistemului este necunoscut"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Пожалуйста, подождите", "wait": "Пожалуйста, подождите",
"empty_data": "Статус подсистемы неизвестен" "empty_data": "Статус подсистемы неизвестен"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Čakajte, prosím", "wait": "Čakajte, prosím",
"empty_data": "Stav podsystému neznámy" "empty_data": "Stav podsystému neznámy"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Prijaté", "rx": "Prijaté",
"tx": "Odoslané", "tx": "Odoslané",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Neznani status podsistema" "empty_data": "Neznani status podsistema"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Молим сачекајте", "wait": "Молим сачекајте",
"empty_data": "Статус подсистема непознат" "empty_data": "Статус подсистема непознат"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "Subsystem status unknown" "empty_data": "Subsystem status unknown"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Lütfen bekleyin", "wait": "Lütfen bekleyin",
"empty_data": "Alt sistem durumu bilinmiyor" "empty_data": "Alt sistem durumu bilinmiyor"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "Gelen Veri", "rx": "Gelen Veri",
"tx": "Giden Veri", "tx": "Giden Veri",

View File

@@ -66,6 +66,11 @@
"wait": "Будь ласка, зачекайте", "wait": "Будь ласка, зачекайте",
"empty_data": "Статус підсистеми невідомий" "empty_data": "Статус підсистеми невідомий"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Vui lòng chờ", "wait": "Vui lòng chờ",
"empty_data": "Trạng thái hệ thống phụ không xác định" "empty_data": "Trạng thái hệ thống phụ không xác định"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "RX", "rx": "RX",
"tx": "TX", "tx": "TX",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "子系統狀態未知" "empty_data": "子系統狀態未知"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "接收", "rx": "接收",
"tx": "發送", "tx": "發送",

View File

@@ -66,6 +66,11 @@
"wait": "请稍候", "wait": "请稍候",
"empty_data": "子系统状态未知" "empty_data": "子系统状态未知"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "接收", "rx": "接收",
"tx": "发送", "tx": "发送",

View File

@@ -66,6 +66,11 @@
"wait": "Please wait", "wait": "Please wait",
"empty_data": "子系統狀態未知" "empty_data": "子系統狀態未知"
}, },
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
},
"docker": { "docker": {
"rx": "接收", "rx": "接收",
"tx": "傳送", "tx": "傳送",

View File

@@ -92,6 +92,23 @@ describe("pages/api/widgets/glances", () => {
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
}); });
it("falls back to version 3 when version is invalid", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({ url: "http://glances" });
httpProxy
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify({ total: 1 }))])
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify({ avg: 2 }))])
.mockResolvedValueOnce([200, null, Buffer.from(JSON.stringify({ available: 3 }))]);
const req = { query: { index: "0", version: "3/../../secret-endpoint" } };
const res = createMockRes();
await handler(req, res);
expect(httpProxy).toHaveBeenCalledWith("http://glances/api/3/cpu", expect.any(Object));
expect(res.statusCode).toBe(200);
});
it("returns 400 when glances returns 401", async () => { it("returns 400 when glances returns 401", async () => {
getPrivateWidgetOptions.mockResolvedValueOnce({ url: "http://glances" }); getPrivateWidgetOptions.mockResolvedValueOnce({ url: "http://glances" });
httpProxy.mockResolvedValueOnce([401, null, Buffer.from("nope")]); httpProxy.mockResolvedValueOnce([401, null, Buffer.from("nope")]);

View File

@@ -1,5 +1,6 @@
import { getPrivateWidgetOptions } from "utils/config/widget-helpers"; import { getPrivateWidgetOptions } from "utils/config/widget-helpers";
import createLogger from "utils/logger"; import createLogger from "utils/logger";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http"; import { httpProxy } from "utils/proxy/http";
const logger = createLogger("glances"); const logger = createLogger("glances");
@@ -45,7 +46,7 @@ export default async function handler(req, res) {
const { index, cputemp: includeCpuTemp, uptime: includeUptime, disk: includeDisks, version } = req.query; const { index, cputemp: includeCpuTemp, uptime: includeUptime, disk: includeDisks, version } = req.query;
const privateWidgetOptions = await getPrivateWidgetOptions("glances", index); const privateWidgetOptions = await getPrivateWidgetOptions("glances", index);
privateWidgetOptions.version = version ?? 3; privateWidgetOptions.version = parseVersionForUrl(version, 3);
try { try {
const cpuData = await retrieveFromGlancesAPI(privateWidgetOptions, "cpu"); const cpuData = await retrieveFromGlancesAPI(privateWidgetOptions, "cpu");

View File

@@ -10,6 +10,7 @@ import { getKubeConfig } from "utils/config/kubernetes";
import * as shvl from "utils/config/shvl"; import * as shvl from "utils/config/shvl";
import kubernetes from "utils/kubernetes/export"; import kubernetes from "utils/kubernetes/export";
import createLogger from "utils/logger"; import createLogger from "utils/logger";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
const logger = createLogger("service-helpers"); const logger = createLogger("service-helpers");
@@ -113,7 +114,7 @@ export async function servicesFromDocker() {
} }
let substitutedVal = substituteEnvironmentVars(containerLabels[label]); let substitutedVal = substituteEnvironmentVars(containerLabels[label]);
if (value === "widget.version" || /^widgets\[\d+\]\.version$/.test(value)) { if (value === "widget.version" || /^widgets\[\d+\]\.version$/.test(value)) {
substitutedVal = parseInt(substitutedVal, 10); substitutedVal = parseVersionForUrl(substitutedVal);
} }
shvl.set(constructedService, value, substitutedVal); shvl.set(constructedService, value, substitutedVal);
} }
@@ -590,7 +591,7 @@ export function cleanServiceGroups(groups) {
"vikunja", "vikunja",
].includes(type) ].includes(type)
) { ) {
if (version) widget.version = parseInt(version, 10); widget.version = parseVersionForUrl(version);
} }
if (type === "glances") { if (type === "glances") {
if (metric) widget.metric = metric; if (metric) widget.metric = metric;

View File

@@ -12,6 +12,22 @@ export function formatApiCall(url, args) {
return url.replace(find, replace).replace(find, replace); return url.replace(find, replace).replace(find, replace);
} }
export function parseVersionForUrl(version, defaultValue = null) {
if (version === undefined || version === null || version === "") {
return defaultValue;
}
if (typeof version === "number") {
return Number.isInteger(version) && version >= 0 ? version : defaultValue;
}
if (typeof version === "string" && /^\d+$/.test(version)) {
return Number(version);
}
return defaultValue;
}
export function getURLSearchParams(widget, endpoint) { export function getURLSearchParams(widget, endpoint) {
const params = new URLSearchParams({ const params = new URLSearchParams({
group: widget.service_group, group: widget.service_group,

View File

@@ -7,6 +7,7 @@ import {
getURLSearchParams, getURLSearchParams,
jsonArrayFilter, jsonArrayFilter,
jsonArrayTransform, jsonArrayTransform,
parseVersionForUrl,
sanitizeErrorURL, sanitizeErrorURL,
} from "./api-helpers"; } from "./api-helpers";
@@ -21,6 +22,20 @@ describe("utils/proxy/api-helpers", () => {
expect(formatApiCall("{a}-{a}-{missing}", { a: "x" })).toBe("x-x-"); expect(formatApiCall("{a}-{a}-{missing}", { a: "x" })).toBe("x-x-");
}); });
it("parseVersionForUrl accepts canonical non-negative integers", () => {
expect(parseVersionForUrl("3")).toBe(3);
expect(parseVersionForUrl(4)).toBe(4);
expect(parseVersionForUrl(undefined, 3)).toBe(3);
});
it("parseVersionForUrl rejects non-canonical values", () => {
expect(parseVersionForUrl("3/../../path", 3)).toBe(3);
expect(parseVersionForUrl("1e2", 3)).toBe(3);
expect(parseVersionForUrl("0x10", 3)).toBe(3);
expect(parseVersionForUrl(-1, 3)).toBe(3);
expect(parseVersionForUrl(Number.NaN, 3)).toBe(3);
});
it("getURLSearchParams includes group/service/index and optionally endpoint", () => { it("getURLSearchParams includes group/service/index and optionally endpoint", () => {
const widget = { service_group: "g", service_name: "s", index: "0" }; const widget = { service_group: "g", service_name: "s", index: "0" };

View File

@@ -2,7 +2,7 @@ import cache from "memory-cache";
import getServiceWidget from "utils/config/service-helpers"; import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger"; import createLogger from "utils/logger";
import { asJson, formatApiCall } from "utils/proxy/api-helpers"; import { asJson, formatApiCall, parseVersionForUrl } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http"; import { httpProxy } from "utils/proxy/http";
import widgets from "widgets/widgets"; import widgets from "widgets/widgets";
@@ -56,7 +56,7 @@ async function getApiInfo(serviceWidget, apiName, serviceName) {
const json = asJson(data); const json = asJson(data);
if (json?.data?.[apiName]) { if (json?.data?.[apiName]) {
cgiPath = json.data[apiName].path; cgiPath = json.data[apiName].path;
maxVersion = json.data[apiName].maxVersion; maxVersion = parseVersionForUrl(json.data[apiName].maxVersion);
logger.debug( logger.debug(
`Detected ${serviceWidget.type}: apiName '${apiName}', cgiPath '${cgiPath}', and maxVersion ${maxVersion}`, `Detected ${serviceWidget.type}: apiName '${apiName}', cgiPath '${cgiPath}', and maxVersion ${maxVersion}`,
); );

View File

@@ -147,6 +147,7 @@ const components = {
tubearchivist: dynamic(() => import("./tubearchivist/component")), tubearchivist: dynamic(() => import("./tubearchivist/component")),
truenas: dynamic(() => import("./truenas/component")), truenas: dynamic(() => import("./truenas/component")),
unifi: dynamic(() => import("./unifi/component")), unifi: dynamic(() => import("./unifi/component")),
unifi_drive: dynamic(() => import("./unifi_drive/component")),
unmanic: dynamic(() => import("./unmanic/component")), unmanic: dynamic(() => import("./unmanic/component")),
unraid: dynamic(() => import("./unraid/component")), unraid: dynamic(() => import("./unraid/component")),
uptimekuma: dynamic(() => import("./uptimekuma/component")), uptimekuma: dynamic(() => import("./uptimekuma/component")),

View File

@@ -12,7 +12,7 @@ async function login(widget) {
const loginParams = { const loginParams = {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: null, body: "{}",
}; };
if (widget.username && widget.password) { if (widget.username && widget.password) {

View File

@@ -45,7 +45,7 @@ describe("widgets/flood/proxy", () => {
expect(httpProxy).toHaveBeenCalledTimes(3); expect(httpProxy).toHaveBeenCalledTimes(3);
expect(httpProxy.mock.calls[0][0].toString()).toBe("http://flood/api/stats"); expect(httpProxy.mock.calls[0][0].toString()).toBe("http://flood/api/stats");
expect(httpProxy.mock.calls[1][0]).toBe("http://flood/api/auth/authenticate"); expect(httpProxy.mock.calls[1][0]).toBe("http://flood/api/auth/authenticate");
expect(httpProxy.mock.calls[1][1].body).toBeNull(); expect(httpProxy.mock.calls[1][1].body).toBe("{}");
expect(httpProxy.mock.calls[2][0].toString()).toBe("http://flood/api/stats"); expect(httpProxy.mock.calls[2][0].toString()).toBe("http://flood/api/stats");
expect(res.statusCode).toBe(200); expect(res.statusCode).toBe(200);
expect(res.body).toEqual(Buffer.from("data")); expect(res.body).toEqual(Buffer.from("data"));

View File

@@ -4,6 +4,7 @@ import { useTranslation } from "next-i18next";
import Block from "../components/block"; import Block from "../components/block";
import Container from "../components/container"; import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
const statusMap = { const statusMap = {
@@ -19,11 +20,12 @@ export default function Component({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { chart, refreshInterval = defaultInterval, version = 3 } = widget; const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const idKey = version === 3 ? "Id" : "id"; const idKey = apiVersion === 3 ? "Id" : "id";
const statusKey = version === 3 ? "Status" : "status"; const statusKey = apiVersion === 3 ? "Status" : "status";
const { data, error } = useWidgetAPI(service.widget, `${version}/containers`, { const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/containers`, {
refreshInterval: Math.max(defaultInterval, refreshInterval), refreshInterval: Math.max(defaultInterval, refreshInterval),
}); });

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import Block from "../components/block"; import Block from "../components/block";
import Container from "../components/container"; import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
const Chart = dynamic(() => import("../components/chart"), { ssr: false }); const Chart = dynamic(() => import("../components/chart"), { ssr: false });
@@ -16,14 +17,15 @@ export default function Component({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget; const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit)); const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(service.widget, `${version}/cpu`, { const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/cpu`, {
refreshInterval: Math.max(defaultInterval, refreshInterval), refreshInterval: Math.max(defaultInterval, refreshInterval),
}); });
const { data: quicklookData, error: quicklookError } = useWidgetAPI(service.widget, `${version}/quicklook`); const { data: quicklookData, error: quicklookError } = useWidgetAPI(service.widget, `${apiVersion}/quicklook`);
useEffect(() => { useEffect(() => {
if (data) { if (data) {

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import Block from "../components/block"; import Block from "../components/block";
import Container from "../components/container"; import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false }); const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
@@ -16,6 +17,7 @@ export default function Component({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget; const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [, diskName] = widget.metric.split(":"); const [, diskName] = widget.metric.split(":");
const [dataPoints, setDataPoints] = useState( const [dataPoints, setDataPoints] = useState(
@@ -23,7 +25,7 @@ export default function Component({ service }) {
); );
const [ratePoints, setRatePoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit)); const [ratePoints, setRatePoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(service.widget, `${version}/diskio`, { const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/diskio`, {
refreshInterval: Math.max(defaultInterval, refreshInterval), refreshInterval: Math.max(defaultInterval, refreshInterval),
}); });

View File

@@ -3,6 +3,7 @@ import { useTranslation } from "next-i18next";
import Block from "../components/block"; import Block from "../components/block";
import Container from "../components/container"; import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
const defaultInterval = 1000; const defaultInterval = 1000;
@@ -11,10 +12,11 @@ export default function Component({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { chart, refreshInterval = defaultInterval, version = 3 } = widget; const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [, fsName] = widget.metric.split("fs:"); const [, fsName] = widget.metric.split("fs:");
const diskUnits = widget.diskUnits === "bbytes" ? "common.bbytes" : "common.bytes"; const diskUnits = widget.diskUnits === "bbytes" ? "common.bbytes" : "common.bytes";
const { data, error } = useWidgetAPI(widget, `${version}/fs`, { const { data, error } = useWidgetAPI(widget, `${apiVersion}/fs`, {
refreshInterval: Math.max(defaultInterval, refreshInterval), refreshInterval: Math.max(defaultInterval, refreshInterval),
}); });

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import Block from "../components/block"; import Block from "../components/block";
import Container from "../components/container"; import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false }); const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
@@ -16,11 +17,12 @@ export default function Component({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget; const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [, gpuName] = widget.metric.split(":"); const [, gpuName] = widget.metric.split(":");
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit)); const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(widget, `${version}/gpu`, { const { data, error } = useWidgetAPI(widget, `${apiVersion}/gpu`, {
refreshInterval: Math.max(defaultInterval, refreshInterval), refreshInterval: Math.max(defaultInterval, refreshInterval),
}); });

View File

@@ -3,6 +3,7 @@ import { useTranslation } from "next-i18next";
import Block from "../components/block"; import Block from "../components/block";
import Container from "../components/container"; import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
function Swap({ quicklookData, className = "" }) { function Swap({ quicklookData, className = "" }) {
@@ -75,12 +76,13 @@ const defaultSystemInterval = 30000; // This data (OS, hostname, distribution) i
export default function Component({ service }) { export default function Component({ service }) {
const { widget } = service; const { widget } = service;
const { chart, refreshInterval = defaultInterval, version = 3 } = widget; const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const { data: quicklookData, errorL: quicklookError } = useWidgetAPI(service.widget, `${version}/quicklook`, { const { data: quicklookData, errorL: quicklookError } = useWidgetAPI(service.widget, `${apiVersion}/quicklook`, {
refreshInterval, refreshInterval,
}); });
const { data: systemData, errorL: systemError } = useWidgetAPI(service.widget, `${version}/system`, { const { data: systemData, errorL: systemError } = useWidgetAPI(service.widget, `${apiVersion}/system`, {
refreshInterval: defaultSystemInterval, refreshInterval: defaultSystemInterval,
}); });

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import Block from "../components/block"; import Block from "../components/block";
import Container from "../components/container"; import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false }); const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
@@ -17,10 +18,11 @@ export default function Component({ service }) {
const { widget } = service; const { widget } = service;
const { chart } = widget; const { chart } = widget;
const { refreshInterval = defaultInterval(chart), pointsLimit = defaultPointsLimit, version = 3 } = widget; const { refreshInterval = defaultInterval(chart), pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit)); const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(service.widget, `${version}/mem`, { const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/mem`, {
refreshInterval: Math.max(defaultInterval(chart), refreshInterval), refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
}); });

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import Block from "../components/block"; import Block from "../components/block";
import Container from "../components/container"; import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false }); const ChartDual = dynamic(() => import("../components/chart_dual"), { ssr: false });
@@ -17,15 +18,16 @@ export default function Component({ service }) {
const { widget } = service; const { widget } = service;
const { chart, metric } = widget; const { chart, metric } = widget;
const { refreshInterval = defaultInterval(chart), pointsLimit = defaultPointsLimit, version = 3 } = widget; const { refreshInterval = defaultInterval(chart), pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const rxKey = version === 3 ? "rx" : "bytes_recv"; const rxKey = apiVersion === 3 ? "rx" : "bytes_recv";
const txKey = version === 3 ? "tx" : "bytes_sent"; const txKey = apiVersion === 3 ? "tx" : "bytes_sent";
const [, interfaceName] = metric.split(":"); const [, interfaceName] = metric.split(":");
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit)); const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(widget, `${version}/network`, { const { data, error } = useWidgetAPI(widget, `${apiVersion}/network`, {
refreshInterval: Math.max(defaultInterval(chart), refreshInterval), refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
}); });

View File

@@ -4,6 +4,7 @@ import { useTranslation } from "next-i18next";
import Block from "../components/block"; import Block from "../components/block";
import Container from "../components/container"; import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
const statusMap = { const statusMap = {
@@ -22,10 +23,11 @@ export default function Component({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { chart, refreshInterval = defaultInterval, version = 3 } = widget; const { chart, refreshInterval = defaultInterval, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const memoryInfoKey = version === 3 ? 0 : "rss"; const memoryInfoKey = apiVersion === 3 ? 0 : "rss";
const { data, error } = useWidgetAPI(service.widget, `${version}/processlist`, { const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/processlist`, {
refreshInterval: Math.max(defaultInterval, refreshInterval), refreshInterval: Math.max(defaultInterval, refreshInterval),
}); });

View File

@@ -5,6 +5,7 @@ import { useEffect, useState } from "react";
import Block from "../components/block"; import Block from "../components/block";
import Container from "../components/container"; import Container from "../components/container";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api"; import useWidgetAPI from "utils/proxy/use-widget-api";
const Chart = dynamic(() => import("../components/chart"), { ssr: false }); const Chart = dynamic(() => import("../components/chart"), { ssr: false });
@@ -16,11 +17,12 @@ export default function Component({ service }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { widget } = service; const { widget } = service;
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget; const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
const [, sensorName] = widget.metric.split(":"); const [, sensorName] = widget.metric.split(":");
const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit)); const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
const { data, error } = useWidgetAPI(service.widget, `${version}/sensors`, { const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/sensors`, {
refreshInterval: Math.max(defaultInterval, refreshInterval), refreshInterval: Math.max(defaultInterval, refreshInterval),
}); });

View File

@@ -3,7 +3,7 @@ import credentialedProxyHandler from "utils/proxy/handlers/credentialed";
const widget = { const widget = {
api: "{url}/api/{endpoint}", api: "{url}/api/{endpoint}",
proxyHandler: credentialedProxyHandler, proxyHandler: credentialedProxyHandler,
allowedEndpoints: /\d\/quicklook|diskio|cpu|fs|gpu|system|mem|network|processlist|sensors|containers/, allowedEndpoints: /^\d+\/(quicklook|diskio|cpu|fs|gpu|system|mem|network|processlist|sensors|containers)$/,
}; };
export default widget; export default widget;

View File

@@ -8,6 +8,10 @@ describe("glances widget config", () => {
it("exports a valid widget config", () => { it("exports a valid widget config", () => {
expectWidgetConfigShape(widget); expectWidgetConfigShape(widget);
expect(widget.allowedEndpoints?.test("3/quicklook")).toBe(true); expect(widget.allowedEndpoints?.test("3/quicklook")).toBe(true);
expect(widget.allowedEndpoints?.test("12/cpu")).toBe(true);
expect(widget.allowedEndpoints?.test("unknown")).toBe(false); expect(widget.allowedEndpoints?.test("unknown")).toBe(false);
expect(widget.allowedEndpoints?.test("xxcpuyy")).toBe(false);
expect(widget.allowedEndpoints?.test("3/cpu/extra")).toBe(false);
expect(widget.allowedEndpoints?.test("membrane")).toBe(false);
}); });
}); });

View File

@@ -0,0 +1,58 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
const { data: storageData, error: storageError } = useWidgetAPI(widget, "storage");
if (storageError) {
return <Container service={service} error={storageError} />;
}
if (!storageData) {
return (
<Container service={service}>
<Block field="unifi_drive.total" label="resources.total" />
<Block field="unifi_drive.used" label="resources.used" />
<Block field="unifi_drive.available" label="resources.free" />
<Block field="unifi_drive.status" label="widget.status" />
</Container>
);
}
const { data: storage } = storageData;
if (!storage) {
return (
<Container service={service}>
<Block value={t("unifi_drive.no_data")} />
</Container>
);
}
const { totalQuota, usage, status } = storage;
const totalBytes = totalQuota ?? 0;
const usedBytes = (usage?.system || 0) + (usage?.myDrives || 0) + (usage?.sharedDrives || 0);
const availableBytes = Math.max(0, totalBytes - usedBytes);
let statusValue = status;
if (status === "healthy") statusValue = t("unifi_drive.healthy");
else if (status === "degraded") statusValue = t("unifi_drive.degraded");
return (
<Container service={service}>
<Block field="unifi_drive.total" label="resources.total" value={t("common.bytes", { value: totalBytes })} />
<Block field="unifi_drive.used" label="resources.used" value={t("common.bytes", { value: usedBytes })} />
<Block
field="unifi_drive.available"
label="resources.free"
value={t("common.bytes", { value: availableBytes })}
/>
<Block field="unifi_drive.status" label="widget.status" value={statusValue} />
</Container>
);
}

View File

@@ -0,0 +1,92 @@
// @vitest-environment jsdom
import { screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils/render-with-providers";
import { expectBlockValue } from "test-utils/widget-assertions";
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
import Component from "./component";
describe("widgets/unifi_drive/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholders while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const service = { widget: { type: "unifi_drive" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("resources.total")).toBeInTheDocument();
expect(screen.getByText("resources.used")).toBeInTheDocument();
expect(screen.getByText("resources.free")).toBeInTheDocument();
expect(screen.getByText("widget.status")).toBeInTheDocument();
});
it("renders error when API fails", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: new Error("fail") });
const service = { widget: { type: "unifi_drive" } };
renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(screen.getAllByText("widget.api_error", { exact: false }).length).toBeGreaterThan(0);
});
it("renders no_data when storage data is missing", () => {
useWidgetAPI.mockReturnValue({ data: { data: null }, error: undefined });
const service = { widget: { type: "unifi_drive" } };
renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(screen.getByText("unifi_drive.no_data")).toBeInTheDocument();
});
it("renders storage statistics when data is loaded", () => {
useWidgetAPI.mockReturnValue({
data: {
data: {
totalQuota: 1000000000000,
usage: { system: 100000000000, myDrives: 200000000000, sharedDrives: 50000000000 },
status: "healthy",
},
},
error: undefined,
});
const service = { widget: { type: "unifi_drive" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expectBlockValue(container, "resources.total", 1000000000000);
expectBlockValue(container, "resources.used", 350000000000);
expectBlockValue(container, "resources.free", 650000000000);
expectBlockValue(container, "widget.status", "unifi_drive.healthy");
});
it("renders degraded status", () => {
useWidgetAPI.mockReturnValue({
data: {
data: {
totalQuota: 100,
usage: { system: 10, myDrives: 20, sharedDrives: 5 },
status: "degraded",
},
},
error: undefined,
});
const service = { widget: { type: "unifi_drive" } };
const { container } = renderWithProviders(<Component service={service} />, { settings: { hideErrors: false } });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expectBlockValue(container, "widget.status", "unifi_drive.degraded");
expectBlockValue(container, "resources.free", 65);
});
});

View File

@@ -0,0 +1,36 @@
import getServiceWidget from "utils/config/service-helpers";
import createUnifiProxyHandler from "utils/proxy/handlers/unifi";
import { httpProxy } from "utils/proxy/http";
const drivePrefix = "/proxy/drive";
async function getWidget(req, logger) {
const { group, service, index } = req.query;
if (!group || !service) return null;
const widget = await getServiceWidget(group, service, index);
if (!widget) {
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
return null;
}
return widget;
}
async function resolveRequestContext({ cachedPrefix, widget }) {
if (cachedPrefix !== null) {
return { prefix: cachedPrefix };
}
const [, , , responseHeaders] = await httpProxy(widget.url);
return {
prefix: drivePrefix,
csrfToken: responseHeaders?.["x-csrf-token"],
};
}
export default createUnifiProxyHandler({
proxyName: "unifiDriveProxyHandler",
resolveWidget: getWidget,
resolveRequestContext,
});

View File

@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, cache, logger } = vi.hoisted(() => {
const store = new Map();
return {
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
cache: {
get: vi.fn((k) => (store.has(k) ? store.get(k) : null)),
put: vi.fn((k, v) => store.set(k, v)),
del: vi.fn((k) => store.delete(k)),
_reset: () => store.clear(),
},
logger: { debug: vi.fn(), error: vi.fn() },
};
});
vi.mock("memory-cache", () => ({ default: cache, ...cache }));
vi.mock("utils/logger", () => ({ default: () => logger }));
vi.mock("utils/config/service-helpers", () => ({ default: getServiceWidget }));
vi.mock("utils/proxy/http", () => ({ httpProxy }));
vi.mock("widgets/widgets", () => ({
default: { unifi_drive: { api: "{url}{prefix}/api/{endpoint}" } },
}));
import unifiDriveProxyHandler from "./proxy";
const widgetConfig = { type: "unifi_drive", url: "http://unifi", username: "u", password: "p" };
describe("widgets/unifi_drive/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
cache._reset();
});
it("returns 400 when widget config is missing", async () => {
getServiceWidget.mockResolvedValue(null);
const res = createMockRes();
await unifiDriveProxyHandler(
{ query: { group: "g", service: "s", endpoint: "v1/systems/storage?type=detail" } },
res,
);
expect(res.statusCode).toBe(400);
});
it("returns 403 when widget type has no API config", async () => {
getServiceWidget.mockResolvedValue({ ...widgetConfig, type: "unknown" });
const res = createMockRes();
await unifiDriveProxyHandler({ query: { group: "g", service: "s", endpoint: "storage" } }, res);
expect(res.statusCode).toBe(403);
});
it("uses /proxy/drive prefix and returns data on success", async () => {
getServiceWidget.mockResolvedValue({ ...widgetConfig });
httpProxy
.mockResolvedValueOnce([200, "text/html", Buffer.from(""), {}])
.mockResolvedValueOnce([200, "application/json", Buffer.from('{"data":{}}'), {}]);
const res = createMockRes();
await unifiDriveProxyHandler({ query: { group: "g", service: "s", endpoint: "storage" } }, res);
expect(httpProxy.mock.calls[0][0]).toBe("http://unifi");
expect(httpProxy.mock.calls[1][0].toString()).toContain("/proxy/drive/api/");
expect(cache.put).toHaveBeenCalledWith("unifiDriveProxyHandler__prefix.s", "/proxy/drive");
expect(res.statusCode).toBe(200);
});
it("skips prefix detection when cached", async () => {
getServiceWidget.mockResolvedValue({ ...widgetConfig });
cache.put("unifiDriveProxyHandler__prefix.s", "/proxy/drive");
httpProxy.mockResolvedValueOnce([200, "application/json", Buffer.from('{"data":{}}'), {}]);
const res = createMockRes();
await unifiDriveProxyHandler({ query: { group: "g", service: "s", endpoint: "storage" } }, res);
expect(httpProxy).toHaveBeenCalledTimes(1);
expect(httpProxy.mock.calls[0][0].toString()).toContain("/proxy/drive/api/");
expect(res.statusCode).toBe(200);
});
});

View File

@@ -0,0 +1,14 @@
import unifiDriveProxyHandler from "./proxy";
const widget = {
api: "{url}{prefix}/api/{endpoint}",
proxyHandler: unifiDriveProxyHandler,
mappings: {
storage: {
endpoint: "v1/systems/storage?type=detail",
},
},
};
export default widget;

View File

@@ -0,0 +1,11 @@
import { describe, it } from "vitest";
import { expectWidgetConfigShape } from "test-utils/widget-config";
import widget from "./widget";
describe("unifi_drive widget config", () => {
it("exports a valid widget config", () => {
expectWidgetConfigShape(widget);
});
});

View File

@@ -137,6 +137,7 @@ import trilium from "./trilium/widget";
import truenas from "./truenas/widget"; import truenas from "./truenas/widget";
import tubearchivist from "./tubearchivist/widget"; import tubearchivist from "./tubearchivist/widget";
import unifi from "./unifi/widget"; import unifi from "./unifi/widget";
import unifi_drive from "./unifi_drive/widget";
import unmanic from "./unmanic/widget"; import unmanic from "./unmanic/widget";
import unraid from "./unraid/widget"; import unraid from "./unraid/widget";
import uptimekuma from "./uptimekuma/widget"; import uptimekuma from "./uptimekuma/widget";
@@ -296,6 +297,7 @@ const widgets = {
truenas, truenas,
unifi, unifi,
unifi_console: unifi, unifi_console: unifi,
unifi_drive,
unmanic, unmanic,
unraid, unraid,
uptimekuma, uptimekuma,