Compare commits

..

22 Commits

Author SHA1 Message Date
Crowdin Bot
0ca82155a1 New Crowdin translations by GitHub Action
Some checks are pending
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-11 00:51:38 +00:00
shamoon
1accddf29a Fix: prevent omada race conditions with auth and cookie caching (#6549)
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
2026-04-10 15:48:38 -07:00
dependabot[bot]
636d62106c Chore(deps): Bump next from 16.1.7 to 16.2.3 in the npm_and_yarn group across 1 directory (#6547)
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 (2) (push) Has been cancelled
Tests / vitest (1) (push) Has been cancelled
Tests / vitest (3) (push) Has been cancelled
Tests / vitest (4) (push) Has been cancelled
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 09:47:20 -07:00
shamoon
d048888d99 Trigger Docker publish on fix/** branches 2026-04-10 09:24:05 -07:00
dependabot[bot]
24804f39fc Chore(deps): Bump docker/login-action from 4.0.0 to 4.1.0 (#6540)
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
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 00:23:32 +00:00
dependabot[bot]
70f8c67d3c Chore(deps): Bump astral-sh/setup-uv from 7.6.0 to 8.0.0 (#6541)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 00:19:12 +00:00
dependabot[bot]
038f5c3b9f Chore(deps): Bump actions/deploy-pages from 4.0.5 to 5.0.0 (#6542)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-10 00:15:15 +00:00
dependabot[bot]
ade4c733c7 Chore(deps): Bump codecov/codecov-action from 5.5.4 to 6.0.0 (#6543)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 17:11:26 -07:00
dependabot[bot]
bdbd2e6ff0 Chore(deps): Bump vite from 7.3.1 to 7.3.2 in the npm_and_yarn group across 1 directory (#6539)
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 (4) (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
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 22:44:10 -07:00
shamoon
5f4b0b4e33 Enhancement: Cache and reuse keep-alive HTTP(S) agents (#6536)
Some checks failed
Release Drafter / Auto Label PR (push) Has been cancelled
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
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-04-07 08:04:35 -07:00
Gabe Dunn
a56a05b553 Documentation: add reference to required settings to ingressroute docs (#6527)
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: shamoon <4887959+shamoon@users.noreply.github.com>
2026-04-06 09:45:47 -07:00
shamoon
11d1ceb755 Enhancement: increase resources page size for pangolin widget (#6523)
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-04-05 07:21:06 -07:00
dependabot[bot]
6df1cfdeba Chore(deps): Bump peakoss/anti-slop from a5a4b2440c9de6f65b64f0718a0136a1fdb04f6f to 85daca1880e9e1af197fc06ea03349daf08f4202 (#6507)
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
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-05 04:07:54 +00:00
dependabot[bot]
d64fbae776 Chore(deps): Update release-drafter/release-drafter requirement to 139054aeaa9adc52ab36ddf67437541f039b88e2 (#6504)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-05 04:04:05 +00:00
dependabot[bot]
5f7e5430c4 Chore(deps): Bump actions/configure-pages from 5.0.0 to 6.0.0 (#6506)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-05 03:59:04 +00:00
dependabot[bot]
3cc3986ec3 Chore(deps): Bump pnpm/action-setup from a8198c4bff370c8506180b035930dea56dbd5288 to fc06bc1257f339d1d5d8b3a19a8cae5388b55320 (#6505)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-05 03:55:12 +00:00
dependabot[bot]
5af0ab1686 Chore(deps): Bump astral-sh/setup-uv from 94527f2e458b27549849d47d273a16bec83a01e9 to 37802adc94f370d6bfd71619e3f0bf239e1f3b78 (#6503)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-04 20:49:43 -07:00
dependabot[bot]
f0f5c3c15c Chore(deps-dev): Bump @tailwindcss/forms from 0.5.10 to 0.5.11 (#6500)
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
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
24 changed files with 1140 additions and 534 deletions

View File

@@ -7,6 +7,7 @@ on:
branches:
- main
- feature/**
- fix/**
- dev
tags: [ 'v*.*.*' ]
pull_request:
@@ -60,7 +61,7 @@ jobs:
nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
with:
version: 10
run_install: false
@@ -83,7 +84,7 @@ jobs:
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -91,7 +92,7 @@ jobs:
- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

View File

@@ -25,7 +25,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
- run: sudo apt-get install pngquant
- name: Test Docs Build
run: uv run --frozen zensical build --clean
@@ -37,18 +37,18 @@ jobs:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version-file: ".python-version"
- name: Install uv
uses: astral-sh/setup-uv@94527f2e458b27549849d47d273a16bec83a01e9 # v7
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
- run: sudo apt-get install pngquant
- name: Build Docs
run: uv run --frozen zensical build --clean
- uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
with:
path: site
- uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
- uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
id: deployment

View File

@@ -23,7 +23,7 @@ jobs:
uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
- name: Install pnpm
uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
with:
version: 10
run_install: false

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
with:
version: 9
@@ -28,7 +28,7 @@ jobs:
# Run Vitest directly so `--shard` is parsed as an option
- run: pnpm -s exec vitest run --coverage --shard ${{ matrix.shard }}/4 --pool forks
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info

View File

@@ -122,7 +122,7 @@ Use the `gethomepage.dev/pod-selector` selector to specify the pod used for the
### Traefik IngressRoute support
Homepage can also read ingresses defined using the Traefik IngressRoute custom resource definition. Due to the complex nature of Traefik routing rules, it is required for the `gethomepage.dev/href` annotation to be set:
If enabled (with `traefik: true` in kubernetes.yaml), homepage can also read ingresses defined using the Traefik IngressRoute custom resource definition. Due to the complex nature of Traefik routing rules, it is required for the `gethomepage.dev/href` annotation to be set:
```yaml
apiVersion: traefik.io/v1alpha1

View File

@@ -18,17 +18,17 @@
"@kubernetes/client-node": "^1.0.0",
"classnames": "^2.5.1",
"compare-versions": "^6.1.1",
"dockerode": "^4.0.7",
"dockerode": "^4.0.10",
"follow-redirects": "^1.15.11",
"gamedig": "^5.3.2",
"i18next": "^25.8.0",
"i18next": "^25.10.9",
"ical.js": "^2.2.1",
"js-yaml": "^4.1.1",
"json-rpc-2.0": "^1.7.0",
"luxon": "^3.6.1",
"memory-cache": "^0.2.0",
"minecraftstatuspinger": "^1.2.2",
"next": "^16.1.7",
"next": "^16.2.3",
"next-i18next": "^15.4.3",
"ping": "^0.4.4",
"pretty-bytes": "^7.1.0",
@@ -47,10 +47,10 @@
"xml-js": "^1.6.11"
},
"devDependencies": {
"@eslint/compat": "^2.0.2",
"@eslint/compat": "^2.0.3",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/postcss": "^4.1.18",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
@@ -64,7 +64,7 @@
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.2.0",
"jsdom": "^28.1.0",
"postcss": "^8.5.6",
"postcss": "^8.5.8",
"prettier": "^3.8.1",
"prettier-plugin-organize-imports": "^4.3.0",
"tailwind-scrollbar": "^4.0.2",

344
pnpm-lock.yaml generated
View File

@@ -21,8 +21,8 @@ importers:
specifier: ^6.1.1
version: 6.1.1
dockerode:
specifier: ^4.0.7
version: 4.0.7
specifier: ^4.0.10
version: 4.0.10
follow-redirects:
specifier: ^1.15.11
version: 1.15.11
@@ -30,8 +30,8 @@ importers:
specifier: ^5.3.2
version: 5.3.2
i18next:
specifier: ^25.8.0
version: 25.8.0(typescript@5.7.3)
specifier: ^25.10.9
version: 25.10.9(typescript@5.7.3)
ical.js:
specifier: ^2.2.1
version: 2.2.1
@@ -51,11 +51,11 @@ importers:
specifier: ^1.2.2
version: 1.2.2
next:
specifier: ^16.1.7
version: 16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
specifier: ^16.2.3
version: 16.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next-i18next:
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.2.3(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:
specifier: ^0.4.4
version: 0.4.4
@@ -73,7 +73,7 @@ importers:
version: 19.2.4(react@19.2.4)
react-i18next:
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:
specifier: ^5.6.0
version: 5.6.0(react@19.2.4)
@@ -103,8 +103,8 @@ importers:
version: 1.6.11
devDependencies:
'@eslint/compat':
specifier: ^2.0.2
version: 2.0.2(eslint@9.25.1(jiti@2.6.1))
specifier: ^2.0.3
version: 2.0.3(eslint@9.25.1(jiti@2.6.1))
'@eslint/eslintrc':
specifier: ^3.3.3
version: 3.3.3
@@ -112,8 +112,8 @@ importers:
specifier: ^9.39.2
version: 9.39.2
'@tailwindcss/forms':
specifier: ^0.5.10
version: 0.5.10(tailwindcss@4.1.18)
specifier: ^0.5.11
version: 0.5.11(tailwindcss@4.1.18)
'@tailwindcss/postcss':
specifier: ^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)
'@vitest/coverage-v8':
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:
specifier: ^9.25.1
version: 9.25.1(jiti@2.6.1)
@@ -154,8 +154,8 @@ importers:
specifier: ^28.1.0
version: 28.1.0
postcss:
specifier: ^8.5.6
version: 8.5.6
specifier: ^8.5.8
version: 8.5.8
prettier:
specifier: ^3.8.1
version: 3.8.1
@@ -173,7 +173,7 @@ importers:
version: 5.7.3
vitest:
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:
osx-temperature-sensor:
specifier: ^1.0.8
@@ -290,8 +290,8 @@ packages:
'@emnapi/core@1.4.0':
resolution: {integrity: sha512-H+N/FqT07NmLmt6OFFtDfwe8PNygprzBikrEMyQfgqSmT0vzE515Pz7R8izwB9q/zsH/MA64AKoul3sA6/CzVg==}
'@emnapi/runtime@1.9.0':
resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==}
'@emnapi/runtime@1.9.2':
resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==}
'@emnapi/wasi-threads@1.0.1':
resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==}
@@ -462,8 +462,8 @@ packages:
resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
'@eslint/compat@2.0.2':
resolution: {integrity: sha512-pR1DoD0h3HfF675QZx0xsyrsU8q70Z/plx7880NOhS02NuWLgBCOMDL787nUeQ7EWLkxv3bPQJaarjcPQb2Dwg==}
'@eslint/compat@2.0.3':
resolution: {integrity: sha512-SjIJhGigp8hmd1YGIBwh7Ovri7Kisl42GYFjrOyHhtfYGGoLW6teYi/5p8W50KSsawUPpuLOSmsq1bD0NGQLBw==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
peerDependencies:
eslint: ^8.40 || 9 || 10
@@ -483,8 +483,8 @@ packages:
resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/core@1.1.0':
resolution: {integrity: sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==}
'@eslint/core@1.1.1':
resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==}
engines: {node: ^20.19.0 || ^22.13.0 || >=24}
'@eslint/eslintrc@3.3.3':
@@ -537,8 +537,8 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@grpc/grpc-js@1.13.4':
resolution: {integrity: sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==}
'@grpc/grpc-js@1.14.3':
resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==}
engines: {node: '>=12.10.0'}
'@grpc/proto-loader@0.7.15':
@@ -546,6 +546,11 @@ packages:
engines: {node: '>=6'}
hasBin: true
'@grpc/proto-loader@0.8.0':
resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==}
engines: {node: '>=6'}
hasBin: true
'@headlessui/react@2.2.9':
resolution: {integrity: sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==}
engines: {node: '>=10'}
@@ -759,56 +764,56 @@ packages:
'@napi-rs/wasm-runtime@0.2.8':
resolution: {integrity: sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg==}
'@next/env@16.1.7':
resolution: {integrity: sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==}
'@next/env@16.2.3':
resolution: {integrity: sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==}
'@next/eslint-plugin-next@15.5.11':
resolution: {integrity: sha512-tS/HYQOjIoX9ZNDQitba/baS8sTvo3ekY6Vgdx5lmhN4jov082bdApIChXr94qhMZHvEciz9DZglFFnhguQp/A==}
'@next/swc-darwin-arm64@16.1.7':
resolution: {integrity: sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==}
'@next/swc-darwin-arm64@16.2.3':
resolution: {integrity: sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@16.1.7':
resolution: {integrity: sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==}
'@next/swc-darwin-x64@16.2.3':
resolution: {integrity: sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@16.1.7':
resolution: {integrity: sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==}
'@next/swc-linux-arm64-gnu@16.2.3':
resolution: {integrity: sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@16.1.7':
resolution: {integrity: sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==}
'@next/swc-linux-arm64-musl@16.2.3':
resolution: {integrity: sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@16.1.7':
resolution: {integrity: sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==}
'@next/swc-linux-x64-gnu@16.2.3':
resolution: {integrity: sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@16.1.7':
resolution: {integrity: sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==}
'@next/swc-linux-x64-musl@16.2.3':
resolution: {integrity: sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@16.1.7':
resolution: {integrity: sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==}
'@next/swc-win32-arm64-msvc@16.2.3':
resolution: {integrity: sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@16.1.7':
resolution: {integrity: sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==}
'@next/swc-win32-x64-msvc@16.2.3':
resolution: {integrity: sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1062,15 +1067,15 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@swc/helpers@0.5.19':
resolution: {integrity: sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA==}
'@swc/helpers@0.5.21':
resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==}
'@szmarczak/http-timer@5.0.1':
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
engines: {node: '>=14.16'}
'@tailwindcss/forms@0.5.10':
resolution: {integrity: sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==}
'@tailwindcss/forms@0.5.11':
resolution: {integrity: sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==}
peerDependencies:
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':
resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==}
'@types/node@24.1.0':
resolution: {integrity: sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==}
'@types/node@25.5.0':
resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==}
'@types/prismjs@1.26.5':
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
@@ -1579,8 +1584,8 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.10.8:
resolution: {integrity: sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==}
baseline-browser-mapping@2.10.17:
resolution: {integrity: sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -1606,8 +1611,8 @@ packages:
buffer@5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
buildcheck@0.0.6:
resolution: {integrity: sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==}
buildcheck@0.0.7:
resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==}
engines: {node: '>=10.0.0'}
bytes@3.1.2:
@@ -1646,8 +1651,8 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
caniuse-lite@1.0.30001780:
resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==}
caniuse-lite@1.0.30001787:
resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==}
chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
@@ -1890,12 +1895,12 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
docker-modem@5.0.6:
resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==}
docker-modem@5.0.7:
resolution: {integrity: sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==}
engines: {node: '>= 8.0'}
dockerode@4.0.7:
resolution: {integrity: sha512-R+rgrSRTRdU5mH14PZTCPZtW/zw3HDWNTS/1ZAQpL/5Upe/ye5K9WQkIysu4wBoiMwKynsz0a8qWuGsHgEvSAA==}
dockerode@4.0.10:
resolution: {integrity: sha512-8L/P9JynLBiG7/coiA4FlQXegHltRqS0a+KqI44P1zgQh8QLHTg7FKOwhkBgSJwZTeHsq30WRoVFLuwkfK0YFg==}
engines: {node: '>= 8.0'}
doctrine@2.1.0:
@@ -2388,10 +2393,10 @@ packages:
i18next-fs-backend@2.6.1:
resolution: {integrity: sha512-eYWTX7QT7kJ0sZyCPK6x1q+R63zvNKv2D6UdbMf15A8vNb2ZLyw4NNNZxPFwXlIv/U+oUtg8SakW6ZgJZcoqHQ==}
i18next@25.8.0:
resolution: {integrity: sha512-urrg4HMFFMQZ2bbKRK7IZ8/CTE7D8H4JRlAwqA2ZwDRFfdd0K/4cdbNNLgfn9mo+I/h9wJu61qJzH7jCFAhUZQ==}
i18next@25.10.9:
resolution: {integrity: sha512-hQY9/bFoQKGlSKMlaCuLR8w1h5JjieqrsnZvEmj1Ja6Ec7fbyc4cTrCsY9mb9Sd8YQ/swsrKz1S9M8AcvVI70w==}
peerDependencies:
typescript: ^5
typescript: ^5 || ^6
peerDependenciesMeta:
typescript:
optional: true
@@ -2862,8 +2867,8 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
nan@2.23.0:
resolution: {integrity: sha512-1UxuyYGdoQHcGg87Lkqm3FzefucTa0NAiOcuRsDmysep3c1LVCRK2krrUDafMWtjSG04htvAmvg96+SDknOmgQ==}
nan@2.26.2:
resolution: {integrity: sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
@@ -2885,8 +2890,8 @@ packages:
react: '>= 17.0.2'
react-i18next: '>= 13.5.0'
next@16.1.7:
resolution: {integrity: sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==}
next@16.2.3:
resolution: {integrity: sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -3043,8 +3048,8 @@ packages:
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
postcss@8.5.8:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
engines: {node: ^10 || ^12 || >=14}
prelude-ls@1.2.1:
@@ -3089,12 +3094,12 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
protobufjs@7.5.3:
resolution: {integrity: sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==}
protobufjs@7.5.4:
resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
engines: {node: '>=12.0.0'}
pump@3.0.3:
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
pump@3.0.4:
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
@@ -3357,8 +3362,8 @@ packages:
split-ca@1.0.1:
resolution: {integrity: sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==}
ssh2@1.16.0:
resolution: {integrity: sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==}
ssh2@1.17.0:
resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==}
engines: {node: '>=10.16.0'}
stable-hash@0.0.5:
@@ -3512,8 +3517,8 @@ packages:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
tar-fs@2.1.3:
resolution: {integrity: sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==}
tar-fs@2.1.4:
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
@@ -3646,8 +3651,8 @@ packages:
undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
undici-types@7.8.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
undici@7.24.4:
resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==}
@@ -3694,8 +3699,8 @@ packages:
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
hasBin: true
vite@7.3.1:
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
vite@7.3.2:
resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -3992,7 +3997,7 @@ snapshots:
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.9.0':
'@emnapi/runtime@1.9.2':
dependencies:
tslib: 2.8.1
optional: true
@@ -4087,9 +4092,9 @@ snapshots:
'@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:
'@eslint/core': 1.1.0
'@eslint/core': 1.1.1
optionalDependencies:
eslint: 9.25.1(jiti@2.6.1)
@@ -4107,7 +4112,7 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
'@eslint/core@1.1.0':
'@eslint/core@1.1.1':
dependencies:
'@types/json-schema': 7.0.15
@@ -4163,16 +4168,23 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
'@grpc/grpc-js@1.13.4':
'@grpc/grpc-js@1.14.3':
dependencies:
'@grpc/proto-loader': 0.7.15
'@grpc/proto-loader': 0.8.0
'@js-sdsl/ordered-map': 4.4.2
'@grpc/proto-loader@0.7.15':
dependencies:
lodash.camelcase: 4.3.0
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
'@headlessui/react@2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
@@ -4283,7 +4295,7 @@ snapshots:
'@img/sharp-wasm32@0.34.5':
dependencies:
'@emnapi/runtime': 1.9.0
'@emnapi/runtime': 1.9.2
optional: true
'@img/sharp-win32-arm64@0.34.5':
@@ -4367,38 +4379,38 @@ snapshots:
'@napi-rs/wasm-runtime@0.2.8':
dependencies:
'@emnapi/core': 1.4.0
'@emnapi/runtime': 1.9.0
'@emnapi/runtime': 1.9.2
'@tybys/wasm-util': 0.9.0
optional: true
'@next/env@16.1.7': {}
'@next/env@16.2.3': {}
'@next/eslint-plugin-next@15.5.11':
dependencies:
fast-glob: 3.3.1
'@next/swc-darwin-arm64@16.1.7':
'@next/swc-darwin-arm64@16.2.3':
optional: true
'@next/swc-darwin-x64@16.1.7':
'@next/swc-darwin-x64@16.2.3':
optional: true
'@next/swc-linux-arm64-gnu@16.1.7':
'@next/swc-linux-arm64-gnu@16.2.3':
optional: true
'@next/swc-linux-arm64-musl@16.1.7':
'@next/swc-linux-arm64-musl@16.2.3':
optional: true
'@next/swc-linux-x64-gnu@16.1.7':
'@next/swc-linux-x64-gnu@16.2.3':
optional: true
'@next/swc-linux-x64-musl@16.1.7':
'@next/swc-linux-x64-musl@16.2.3':
optional: true
'@next/swc-win32-arm64-msvc@16.1.7':
'@next/swc-win32-arm64-msvc@16.2.3':
optional: true
'@next/swc-win32-x64-msvc@16.1.7':
'@next/swc-win32-x64-msvc@16.2.3':
optional: true
'@nodelib/fs.scandir@2.1.5':
@@ -4448,7 +4460,7 @@ snapshots:
'@react-aria/interactions': 3.25.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@react-aria/utils': 3.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@react-types/shared': 3.32.1(react@19.2.4)
'@swc/helpers': 0.5.19
'@swc/helpers': 0.5.21
clsx: 2.1.1
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
@@ -4459,13 +4471,13 @@ snapshots:
'@react-aria/utils': 3.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
'@react-stately/flags': 3.1.2
'@react-types/shared': 3.32.1(react@19.2.4)
'@swc/helpers': 0.5.19
'@swc/helpers': 0.5.21
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@react-aria/ssr@3.9.10(react@19.2.4)':
dependencies:
'@swc/helpers': 0.5.19
'@swc/helpers': 0.5.21
react: 19.2.4
'@react-aria/utils@3.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
@@ -4474,18 +4486,18 @@ snapshots:
'@react-stately/flags': 3.1.2
'@react-stately/utils': 3.10.8(react@19.2.4)
'@react-types/shared': 3.32.1(react@19.2.4)
'@swc/helpers': 0.5.19
'@swc/helpers': 0.5.21
clsx: 2.1.1
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
'@react-stately/flags@3.1.2':
dependencies:
'@swc/helpers': 0.5.19
'@swc/helpers': 0.5.21
'@react-stately/utils@3.10.8(react@19.2.4)':
dependencies:
'@swc/helpers': 0.5.19
'@swc/helpers': 0.5.21
react: 19.2.4
'@react-types/shared@3.32.1(react@19.2.4)':
@@ -4598,7 +4610,7 @@ snapshots:
dependencies:
tslib: 2.8.1
'@swc/helpers@0.5.19':
'@swc/helpers@0.5.21':
dependencies:
tslib: 2.8.1
@@ -4606,7 +4618,7 @@ snapshots:
dependencies:
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:
mini-svg-data-uri: 1.4.4
tailwindcss: 4.1.18
@@ -4677,7 +4689,7 @@ snapshots:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.1.18
'@tailwindcss/oxide': 4.1.18
postcss: 8.5.6
postcss: 8.5.8
tailwindcss: 4.1.18
'@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:
undici-types: 6.20.0
'@types/node@24.1.0':
'@types/node@25.5.0':
dependencies:
undici-types: 7.8.0
undici-types: 7.18.2
'@types/prismjs@1.26.5': {}
@@ -4859,7 +4871,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 8.29.0
'@typescript-eslint/visitor-keys': 8.29.0
debug: 4.4.3
debug: 4.4.1
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
@@ -4932,7 +4944,7 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.3.3':
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:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -4947,7 +4959,7 @@ snapshots:
std-env: 3.10.0
test-exclude: 7.0.1
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:
- supports-color
@@ -4959,13 +4971,13 @@ snapshots:
chai: 5.3.3
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.2(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2)
vite: 7.3.2(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2)
'@vitest/pretty-format@3.2.4':
dependencies:
@@ -5146,7 +5158,7 @@ snapshots:
base64-js@1.5.1: {}
baseline-browser-mapping@2.10.8: {}
baseline-browser-mapping@2.10.17: {}
bcrypt-pbkdf@1.0.2:
dependencies:
@@ -5180,7 +5192,7 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
buildcheck@0.0.6:
buildcheck@0.0.7:
optional: true
bytes@3.1.2: {}
@@ -5223,7 +5235,7 @@ snapshots:
callsites@3.1.0: {}
caniuse-lite@1.0.30001780: {}
caniuse-lite@1.0.30001787: {}
chai@5.3.3:
dependencies:
@@ -5293,8 +5305,8 @@ snapshots:
cpu-features@0.0.10:
dependencies:
buildcheck: 0.0.6
nan: 2.23.0
buildcheck: 0.0.7
nan: 2.26.2
optional: true
cross-spawn@7.0.6:
@@ -5434,23 +5446,23 @@ snapshots:
detect-libc@2.1.2: {}
docker-modem@5.0.6:
docker-modem@5.0.7:
dependencies:
debug: 4.4.1
debug: 4.4.3
readable-stream: 3.6.2
split-ca: 1.0.1
ssh2: 1.16.0
ssh2: 1.17.0
transitivePeerDependencies:
- supports-color
dockerode@4.0.7:
dockerode@4.0.10:
dependencies:
'@balena/dockerignore': 1.0.2
'@grpc/grpc-js': 1.13.4
'@grpc/grpc-js': 1.14.3
'@grpc/proto-loader': 0.7.15
docker-modem: 5.0.6
protobufjs: 7.5.3
tar-fs: 2.1.3
docker-modem: 5.0.7
protobufjs: 7.5.4
tar-fs: 2.1.4
uuid: 10.0.0
transitivePeerDependencies:
- supports-color
@@ -6182,9 +6194,9 @@ snapshots:
i18next-fs-backend@2.6.1: {}
i18next@25.8.0(typescript@5.7.3):
i18next@25.10.9(typescript@5.7.3):
dependencies:
'@babel/runtime': 7.28.6
'@babel/runtime': 7.29.2
optionalDependencies:
typescript: 5.7.3
@@ -6618,7 +6630,7 @@ snapshots:
ms@2.1.3: {}
nan@2.23.0:
nan@2.26.2:
optional: true
nanoid@3.3.11: {}
@@ -6627,39 +6639,39 @@ snapshots:
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.2.3(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:
'@babel/runtime': 7.28.6
'@types/hoist-non-react-statics': 3.3.7(@types/react@19.0.10)
core-js: 3.48.0
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
next: 16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
next: 16.2.3(react-dom@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:
- '@types/react'
next@16.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
next@16.2.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
'@next/env': 16.1.7
'@next/env': 16.2.3
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.8
caniuse-lite: 1.0.30001780
baseline-browser-mapping: 2.10.17
caniuse-lite: 1.0.30001787
postcss: 8.4.31
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
styled-jsx: 5.1.6(react@19.2.4)
optionalDependencies:
'@next/swc-darwin-arm64': 16.1.7
'@next/swc-darwin-x64': 16.1.7
'@next/swc-linux-arm64-gnu': 16.1.7
'@next/swc-linux-arm64-musl': 16.1.7
'@next/swc-linux-x64-gnu': 16.1.7
'@next/swc-linux-x64-musl': 16.1.7
'@next/swc-win32-arm64-msvc': 16.1.7
'@next/swc-win32-x64-msvc': 16.1.7
'@next/swc-darwin-arm64': 16.2.3
'@next/swc-darwin-x64': 16.2.3
'@next/swc-linux-arm64-gnu': 16.2.3
'@next/swc-linux-arm64-musl': 16.2.3
'@next/swc-linux-x64-gnu': 16.2.3
'@next/swc-linux-x64-musl': 16.2.3
'@next/swc-win32-arm64-msvc': 16.2.3
'@next/swc-win32-x64-msvc': 16.2.3
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
@@ -6796,7 +6808,7 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.6:
postcss@8.5.8:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
@@ -6837,7 +6849,7 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
protobufjs@7.5.3:
protobufjs@7.5.4:
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/base64': 1.1.2
@@ -6849,10 +6861,10 @@ snapshots:
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
'@types/node': 24.1.0
'@types/node': 25.5.0
long: 5.3.2
pump@3.0.3:
pump@3.0.4:
dependencies:
end-of-stream: 1.4.5
once: 1.4.0
@@ -6875,11 +6887,11 @@ snapshots:
react: 19.2.4
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:
'@babel/runtime': 7.27.6
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
optionalDependencies:
react-dom: 19.2.4(react@19.2.4)
@@ -7190,13 +7202,13 @@ snapshots:
split-ca@1.0.1: {}
ssh2@1.16.0:
ssh2@1.17.0:
dependencies:
asn1: 0.2.6
bcrypt-pbkdf: 1.0.2
optionalDependencies:
cpu-features: 0.0.10
nan: 2.23.0
nan: 2.26.2
stable-hash@0.0.5: {}
@@ -7356,11 +7368,11 @@ snapshots:
tapable@2.3.0: {}
tar-fs@2.1.3:
tar-fs@2.1.4:
dependencies:
chownr: 1.1.4
mkdirp-classic: 0.5.3
pump: 3.0.3
pump: 3.0.4
tar-stream: 2.2.0
tar-stream@2.2.0:
@@ -7507,7 +7519,7 @@ snapshots:
undici-types@6.20.0: {}
undici-types@7.8.0: {}
undici-types@7.18.2: {}
undici@7.24.4: {}
@@ -7570,13 +7582,13 @@ snapshots:
d3-time: 3.1.0
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:
cac: 6.7.14
debug: 4.4.1
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2)
vite: 7.3.2(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -7591,25 +7603,25 @@ snapshots:
- tsx
- yaml
vite@7.3.1(@types/node@24.1.0)(jiti@2.6.1)(lightningcss@1.30.2):
vite@7.3.2(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2):
dependencies:
esbuild: 0.27.2
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
postcss: 8.5.6
postcss: 8.5.8
rollup: 4.59.0
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 24.1.0
'@types/node': 25.5.0
fsevents: 2.3.3
jiti: 2.6.1
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:
'@types/chai': 5.2.3
'@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.2(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.30.2))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -7627,11 +7639,11 @@ snapshots:
tinyglobby: 0.2.15
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 7.3.1(@types/node@24.1.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: 7.3.2(@types/node@25.5.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
optionalDependencies:
'@types/node': 24.1.0
'@types/node': 25.5.0
jsdom: 28.1.0
transitivePeerDependencies:
- jiti

View File

@@ -67,9 +67,9 @@
"empty_data": "Substelsel status onbekend"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
"healthy": "Gesond",
"degraded": "Gedegradeer",
"no_data": "Geen bergingsdata beskikbaar nie"
},
"docker": {
"rx": "RX",

View File

@@ -67,9 +67,9 @@
"empty_data": "Statut du sous-système inconnu"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
"healthy": "Sain",
"degraded": "Dégradé",
"no_data": "Aucune données de stockage disponible"
},
"docker": {
"rx": "Rx",
@@ -115,12 +115,12 @@
"jellyfin": {
"playing": "En cours",
"transcoding": "En cours d'encodage",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"movies": "Movies",
"series": "Series",
"episodes": "Episodes",
"songs": "Songs"
"bitrate": "Débit",
"no_active": "Aucune lecture en cours",
"movies": "Films",
"series": "Séries",
"episodes": "Épisodes",
"songs": "Chansons"
},
"esphome": {
"offline": "Hors ligne",
@@ -190,11 +190,11 @@
"plex_connection_error": "Vérifier la connexion à Plex"
},
"tracearr": {
"no_active": "No Active Streams",
"streams": "Streams",
"transcodes": "Transcodes",
"directplay": "Direct Play",
"bitrate": "Bitrate"
"no_active": "Aucune lecture en cours",
"streams": "Lectures",
"transcodes": "Transcodages",
"directplay": "Lectures directes",
"bitrate": "Débit"
},
"omada": {
"connectedAp": "APs connectées",
@@ -295,12 +295,12 @@
"available": "Disponible"
},
"seerr": {
"pending": "Pending",
"approved": "Approved",
"available": "Available",
"completed": "Completed",
"processing": "Processing",
"issues": "Open Issues"
"pending": "En attente",
"approved": "Validées",
"available": "Disponibles",
"completed": "Complétées",
"processing": "En traitement",
"issues": "Problèmes non résolus"
},
"netalertx": {
"total": "Total",
@@ -619,13 +619,13 @@
"total": "Total"
},
"pangolin": {
"orgs": "Orgs",
"orgs": "Organisations",
"sites": "Sites",
"resources": "Ressources",
"targets": "Cibles",
"traffic": "Trafique",
"in": "In",
"out": "Out"
"in": "Entrant",
"out": "Sortant"
},
"peanut": {
"battery_charge": "Charge de la batterie",
@@ -724,8 +724,8 @@
"volumeAvailable": "Disponible"
},
"dispatcharr": {
"channels": "Channels",
"streams": "Streams"
"channels": "Chaînes",
"streams": "Lectures"
},
"mylar": {
"series": "Séries",
@@ -816,10 +816,10 @@
"series": "Séries"
},
"booklore": {
"libraries": "Libraries",
"books": "Books",
"reading": "Reading",
"finished": "Finished"
"libraries": "Bibliothèques",
"books": "Livres",
"reading": "En cours de lecture",
"finished": "Terminés"
},
"jdownloader": {
"downloadCount": "File d'attente",
@@ -1160,30 +1160,30 @@
"artists": "Artistes"
},
"arcane": {
"containers": "Containers",
"containers": "Conteneurs",
"images": "Images",
"image_updates": "Image Updates",
"images_unused": "Unused",
"environment_required": "Environment ID Required"
"image_updates": "Mises à jour d'images",
"images_unused": "Inutilisées",
"environment_required": "ID d'environnement requis"
},
"dockhand": {
"running": "Running",
"stopped": "Stopped",
"running": "En cours d'exécution",
"stopped": "Arrêtés",
"cpu": "CPU",
"memory": "Memory",
"memory": "Mémoire",
"images": "Images",
"volumes": "Volumes",
"events_today": "Events Today",
"pending_updates": "Pending Updates",
"events_today": "Événements aujourd'hui",
"pending_updates": "Mises à jour en attente",
"stacks": "Stacks",
"paused": "Paused",
"paused": "En pause",
"total": "Total",
"environment_not_found": "Environment Not Found"
"environment_not_found": "Environnement introuvable"
},
"sparkyfitness": {
"eaten": "Eaten",
"burned": "Burned",
"remaining": "Remaining",
"steps": "Steps"
"eaten": "Mangé",
"burned": "Brûlé",
"remaining": "Restant",
"steps": "Pas"
}
}

View File

@@ -67,9 +67,9 @@
"empty_data": "Stanje podsustava nepoznato"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
"healthy": "Zdrav",
"degraded": "Degradirano",
"no_data": "Nema dostupnih podataka spremišta"
},
"docker": {
"rx": "RX",
@@ -624,8 +624,8 @@
"resources": "Resursi",
"targets": "Ciljevi",
"traffic": "Promet",
"in": "In",
"out": "Out"
"in": "Ulaz",
"out": "Izlaz"
},
"peanut": {
"battery_charge": "Napunjenost baterije",

View File

@@ -61,15 +61,15 @@
"wlan_devices": "Dispositivi WLAN",
"lan_users": "Utenti LAN",
"wlan_users": "Utenti WLAN",
"up": "UP",
"up": "SU",
"down": "DOWN",
"wait": "Please wait",
"wait": "Attendere prego",
"empty_data": "Stato del sottosistema sconosciuto"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
"healthy": "Sano",
"degraded": "Degradato",
"no_data": "Nessun dato di archiviazione disponibile"
},
"docker": {
"rx": "RX",
@@ -88,7 +88,7 @@
"partial": "Parziale"
},
"ping": {
"error": "Error",
"error": "Errore",
"ping": "Ping",
"down": "Down",
"up": "Up",
@@ -96,11 +96,11 @@
},
"siteMonitor": {
"http_status": "Stato HTTP",
"error": "Error",
"error": "Errore",
"response": "Risposta",
"down": "Down",
"up": "Up",
"not_available": "Not Available"
"down": "Giù",
"up": "Su",
"not_available": "Non disponibile"
},
"emby": {
"playing": "In riproduzione",
@@ -113,21 +113,21 @@
"songs": "Canzoni"
},
"jellyfin": {
"playing": "Playing",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"movies": "Movies",
"playing": "In riproduzione",
"transcoding": "Transcodifica",
"bitrate": "Velocità in bit",
"no_active": "Nessuna Trasmissione Attiva",
"movies": "Film",
"series": "Serie",
"episodes": "Episodes",
"songs": "Songs"
"episodes": "Episodi",
"songs": "Canzoni"
},
"esphome": {
"offline": "Offline",
"offline_alt": "Offline",
"offline": "Disconnesso",
"offline_alt": "Disconnesso",
"online": "Online",
"total": "Total",
"unknown": "Unknown"
"total": "Totale",
"unknown": "Sconosciuto"
},
"evcc": {
"pv_power": "Produzione",
@@ -148,7 +148,7 @@
"unread": "Non letto"
},
"fritzbox": {
"connectionStatus": "Status",
"connectionStatus": "Stato",
"connectionStatusUnconfigured": "Non configurato",
"connectionStatusConnecting": "Connessione in corso",
"connectionStatusAuthenticating": "In fase di autenticazione",
@@ -156,11 +156,11 @@
"connectionStatusDisconnecting": "Disconnessione in corso",
"connectionStatusDisconnected": "Disconnesso",
"connectionStatusConnected": "Connesso",
"uptime": "Uptime",
"uptime": "Tempo di funzionamento",
"maxDown": "Max. Down",
"maxUp": "Max. Up",
"down": "Down",
"up": "Up",
"down": "Giù",
"up": "Su",
"received": "Ricevuti",
"sent": "Inviati",
"externalIPAddress": "IP Esterno",
@@ -184,17 +184,17 @@
},
"tautulli": {
"playing": "In riproduzione",
"transcoding": "Transcoding",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"transcoding": "Transcodifica",
"bitrate": "Velocità in bit",
"no_active": "Nessuna Trasmissione Attiva",
"plex_connection_error": "Controllare la connessione a Plex"
},
"tracearr": {
"no_active": "No Active Streams",
"streams": "Streams",
"transcodes": "Transcodes",
"directplay": "Direct Play",
"bitrate": "Bitrate"
"no_active": "Nessuna Trasmissione Attiva",
"streams": "Flussi",
"transcodes": "Transcodifiche",
"directplay": "Riproduzione diretta",
"bitrate": "Velocità in bit"
},
"omada": {
"connectedAp": "AP Connessi",
@@ -211,11 +211,11 @@
"plex": {
"streams": "Trasmissioni attive",
"albums": "Album",
"movies": "Movies",
"movies": "Film",
"tv": "Programmi televisivi"
},
"sabnzbd": {
"rate": "Rate",
"rate": "Rapporto",
"queue": "Coda",
"timeleft": "Tempo Rimanente"
},
@@ -227,13 +227,13 @@
"transmission": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"leech": "In download",
"seed": "Seed"
},
"qbittorrent": {
"download": "Download",
"upload": "Upload",
"leech": "Leech",
"leech": "In download",
"seed": "Seed"
},
"qnap": {
@@ -430,21 +430,21 @@
"medusa": {
"wanted": "Wanted",
"queued": "Queued",
"series": "Series"
"series": "Serie"
},
"minecraft": {
"players": "Giocatori",
"version": "Versione",
"status": "Status",
"status": "Stato",
"up": "Online",
"down": "Offline"
"down": "Disconnesso"
},
"miniflux": {
"read": "Letti",
"unread": "Unread"
"unread": "Non letto"
},
"authentik": {
"users": "Users",
"users": "Utenti",
"loginsLast24H": "Accessi (24h)",
"failedLoginsLast24H": "Accessi Falliti (24h)"
},
@@ -456,19 +456,19 @@
},
"glances": {
"cpu": "CPU",
"load": "Load",
"wait": "Please wait",
"load": "Carico",
"wait": "Attendere prego",
"temp": "TEMP",
"_temp": "Temp.",
"warn": "Avviso",
"uptime": "UP",
"total": "Total",
"free": "Free",
"used": "Used",
"days": "d",
"hours": "h",
"uptime": "SU",
"total": "Totale",
"free": "Libero",
"used": "Utilizzato",
"days": "g",
"hours": "o",
"crit": "Critico",
"read": "Read",
"read": "Letti",
"write": "Scrittura",
"gpu": "GPU",
"mem": "Mem.",
@@ -489,57 +489,57 @@
"1-day": "Prevalentemente Soleggiato",
"1-night": "Prevalentemente Sereno",
"2-day": "Parzialmente Nuvoloso",
"2-night": "Partly Cloudy",
"2-night": "Parzialmente Nuvoloso",
"3-day": "Nuvoloso",
"3-night": "Cloudy",
"3-night": "Nuvoloso",
"45-day": "Nebbioso",
"45-night": "Foggy",
"48-day": "Foggy",
"48-night": "Foggy",
"45-night": "Nebbioso",
"48-day": "Nebbioso",
"48-night": "Nebbioso",
"51-day": "Pioggerella Leggera",
"51-night": "Light Drizzle",
"51-night": "Pioggerella Leggera",
"53-day": "Pioggerella",
"53-night": "Drizzle",
"53-night": "Pioggerella",
"55-day": "Pioggerella Pesante",
"55-night": "Heavy Drizzle",
"55-night": "Pioggerella Pesante",
"56-day": "Leggera Pioggia Gelata",
"56-night": "Light Freezing Drizzle",
"56-night": "Leggera Pioggia Gelata",
"57-day": "Pioggerella Gelata",
"57-night": "Freezing Drizzle",
"57-night": "Pioggerella Gelata",
"61-day": "Pioggia Leggera",
"61-night": "Light Rain",
"61-night": "Pioggia Leggera",
"63-day": "Pioggia",
"63-night": "Rain",
"63-night": "Pioggia",
"65-day": "Pioggia Intensa",
"65-night": "Heavy Rain",
"65-night": "Pioggia Intensa",
"66-day": "Grandine",
"66-night": "Freezing Rain",
"67-day": "Freezing Rain",
"67-night": "Freezing Rain",
"66-night": "Grandine",
"67-day": "Grandine",
"67-night": "Grandine",
"71-day": "Leggera Nevicata",
"71-night": "Light Snow",
"71-night": "Leggera Nevicata",
"73-day": "Neve",
"73-night": "Snow",
"73-night": "Neve",
"75-day": "Nevicata Intensa",
"75-night": "Heavy Snow",
"75-night": "Nevicata Intensa",
"77-day": "Fiocchi di Neve",
"77-night": "Snow Grains",
"77-night": "Fiocchi di Neve",
"80-day": "Leggeri Rovesci",
"80-night": "Light Showers",
"80-night": "Leggeri Rovesci",
"81-day": "Rovesci",
"81-night": "Showers",
"81-night": "Rovesci",
"82-day": "Intensi Rovesci",
"82-night": "Heavy Showers",
"82-night": "Intensi Rovesci",
"85-day": "Rovesci di Neve",
"85-night": "Snow Showers",
"86-day": "Snow Showers",
"86-night": "Snow Showers",
"85-night": "Rovesci di Neve",
"86-day": "Rovesci di Neve",
"86-night": "Rovesci di Neve",
"95-day": "Temporale",
"95-night": "Thunderstorm",
"95-night": "Temporale",
"96-day": "Temporale con grandine",
"96-night": "Thunderstorm With Hail",
"99-day": "Thunderstorm With Hail",
"99-night": "Thunderstorm With Hail"
"96-night": "Temporale con grandine",
"99-day": "Temporale con grandine",
"99-night": "Temporale con grandine"
},
"homebridge": {
"available_update": "Sistema",

View File

@@ -67,9 +67,9 @@
"empty_data": "서브시스템 상태 알 수 없음"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
"healthy": "정상",
"degraded": "성능 저하",
"no_data": "저장소 데이터 없음"
},
"docker": {
"rx": "수신",

View File

@@ -67,9 +67,9 @@
"empty_data": "Status podsystemu nieznany"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
"healthy": "Zdrowy",
"degraded": "Zdegradowany",
"no_data": "Brak danych o miejscu przechowywania"
},
"docker": {
"rx": "Rx",

View File

@@ -67,9 +67,9 @@
"empty_data": "Status do Subsistema desconhecido"
},
"unifi_drive": {
"healthy": "Healthy",
"degraded": "Degraded",
"no_data": "No storage data available"
"healthy": "Saudável",
"degraded": "Degradado",
"no_data": "Sem dados de armazenamento disponíveis"
},
"docker": {
"rx": "RX",
@@ -114,9 +114,9 @@
},
"jellyfin": {
"playing": "Jogando",
"transcoding": "Transcoding",
"transcoding": "Transcodificando",
"bitrate": "Bitrate",
"no_active": "No Active Streams",
"no_active": "Sem Transmissões Ativas",
"movies": "Filmes",
"series": "Séries",
"episodes": "Episódios",
@@ -190,10 +190,10 @@
"plex_connection_error": "Verifique a conexão do Plex"
},
"tracearr": {
"no_active": "No Active Streams",
"streams": "Streams",
"transcodes": "Transcodes",
"directplay": "Direct Play",
"no_active": "Sem Transmissões Ativas",
"streams": "Transmissões",
"transcodes": "Transcodificações",
"directplay": "Reprodução direta",
"bitrate": "Bitrate"
},
"omada": {
@@ -619,13 +619,13 @@
"total": "Total"
},
"pangolin": {
"orgs": "Orgs",
"orgs": "Organizações",
"sites": "Sites",
"resources": "Recursos",
"targets": "Targets",
"traffic": "Traffic",
"in": "In",
"out": "Out"
"targets": "Alvos",
"traffic": "Tráfego",
"in": "Em",
"out": "Saída"
},
"peanut": {
"battery_charge": "Carga da bateria",
@@ -1164,11 +1164,11 @@
"images": "Imagens",
"image_updates": "Atualizações de Imagem",
"images_unused": "Não utilizado",
"environment_required": "Environment ID Required"
"environment_required": "ID do ambiente necessário"
},
"dockhand": {
"running": "Executando",
"stopped": "Stopped",
"stopped": "Parado",
"cpu": "CPU",
"memory": "Memória",
"images": "Imagens",
@@ -1178,12 +1178,12 @@
"stacks": "Pilhas",
"paused": "Pausado",
"total": "Total",
"environment_not_found": "Environment Not Found"
"environment_not_found": "Ambiente não encontrado"
},
"sparkyfitness": {
"eaten": "Eaten",
"burned": "Burned",
"remaining": "Remaining",
"steps": "Steps"
"eaten": "Comido",
"burned": "Queimado",
"remaining": "Restante",
"steps": "Passos"
}
}

View File

@@ -458,7 +458,7 @@
"cpu": "CPU",
"load": "负载",
"wait": "请稍候",
"temp": "转速",
"temp": "温度",
"_temp": "Temp",
"warn": "Warn",
"uptime": "运行时间",

View File

@@ -7,7 +7,10 @@ export function setCookieHeader(url, params) {
const existingCookie = cookieJar.getCookieStringSync(url.toString());
if (existingCookie) {
params.headers = params.headers ?? {};
params.headers[params.cookieHeader ?? "Cookie"] = existingCookie;
const cookieHeader = params.cookieHeader ?? "Cookie";
if (!params.headers[cookieHeader]) {
params.headers[cookieHeader] = existingCookie;
}
}
}

View File

@@ -42,4 +42,16 @@ describe("utils/proxy/cookie-jar", () => {
expect(params.headers.Cookie).toContain("c=d");
});
it("does not overwrite an explicit cookie header", async () => {
const { addCookieToJar, setCookieHeader } = await import("./cookie-jar");
const url = new URL("http://example4.test/path");
addCookieToJar(url, { "set-cookie": ["sid=1; Path=/"] });
const params = { headers: { Cookie: "manual=1" } };
setCookieHeader(url, params);
expect(params.headers.Cookie).toBe("manual=1");
});
});

View File

@@ -224,23 +224,43 @@ function homepageDNSLookupFn() {
};
}
const homepageLookup = homepageDNSLookupFn();
const agentCache = new Map();
function getAgent(protocol, disableIpv6) {
const cacheKey = `${protocol}:${disableIpv6 ? "ipv4" : "auto"}`;
const cachedAgent = agentCache.get(cacheKey);
if (cachedAgent) {
return cachedAgent;
}
const agentOptions = {
keepAlive: true,
...(disableIpv6 ? { family: 4, autoSelectFamily: false } : { autoSelectFamilyAttemptTimeout: 500 }),
lookup: homepageLookup,
};
const agent =
protocol === "https:"
? new https.Agent({ ...agentOptions, rejectUnauthorized: false })
: new http.Agent(agentOptions);
agentCache.set(cacheKey, agent);
return agent;
}
export async function httpProxy(url, params = {}) {
const constructedUrl = new URL(url);
const disableIpv6 = process.env.HOMEPAGE_PROXY_DISABLE_IPV6 === "true";
const agentOptions = {
...(disableIpv6 ? { family: 4, autoSelectFamily: false } : { autoSelectFamilyAttemptTimeout: 500 }),
lookup: homepageDNSLookupFn(),
};
let request = null;
if (constructedUrl.protocol === "https:") {
request = httpsRequest(constructedUrl, {
agent: new https.Agent({ ...agentOptions, rejectUnauthorized: false }),
agent: getAgent(constructedUrl.protocol, disableIpv6),
...params,
});
} else {
request = httpRequest(constructedUrl, {
agent: new http.Agent(agentOptions),
agent: getAgent(constructedUrl.protocol, disableIpv6),
...params,
});
}

View File

@@ -8,6 +8,7 @@ const { state, cache, logger, dns, net, cookieJar } = vi.hoisted(() => ({
body: Buffer.from(""),
},
error: null,
lastAgent: null,
lastAgentOptions: null,
lastRequestParams: null,
lastWrittenBody: null,
@@ -59,6 +60,7 @@ vi.mock("follow-redirects", async () => {
state.lastWrittenBody = chunk;
});
req.end = vi.fn(() => {
state.lastAgent = params?.agent ?? null;
state.lastAgentOptions = params?.agent?.opts ?? null;
if (state.error) {
req.emit("error", state.error);
@@ -104,6 +106,7 @@ describe("utils/proxy/http cachedRequest", () => {
headers: { "content-type": "application/json" },
body: Buffer.from(""),
};
state.lastAgent = null;
state.lastAgentOptions = null;
state.lastRequestParams = null;
state.lastWrittenBody = null;
@@ -307,6 +310,7 @@ describe("utils/proxy/http httpProxy", () => {
headers: { "content-type": "application/json" },
body: Buffer.from("ok"),
};
state.lastAgent = null;
state.lastAgentOptions = null;
state.lastRequestParams = null;
state.lastWrittenBody = null;
@@ -397,6 +401,7 @@ describe("utils/proxy/http httpProxy", () => {
await httpMod.httpProxy("http://example.com");
expect(state.lastAgentOptions.keepAlive).toBe(true);
expect(state.lastAgentOptions.family).toBe(4);
expect(state.lastAgentOptions.autoSelectFamily).toBe(false);
});
@@ -409,6 +414,17 @@ describe("utils/proxy/http httpProxy", () => {
expect(state.lastAgentOptions.rejectUnauthorized).toBe(false);
});
it("reuses the same keep-alive agent for repeated http requests", async () => {
const httpMod = await import("./http");
await httpMod.httpProxy("http://example.com/first");
const firstAgent = state.lastAgent;
await httpMod.httpProxy("http://example.com/second");
expect(state.lastAgentOptions.keepAlive).toBe(true);
expect(state.lastAgent).toBe(firstAgent);
});
it("returns a sanitized error response when the request fails", async () => {
state.error = Object.assign(new Error("boom"), { code: "EHOSTUNREACH" });
const httpMod = await import("./http");

View File

@@ -1,12 +1,40 @@
import cache from "memory-cache";
import getServiceWidget from "utils/config/service-helpers";
import createLogger from "utils/logger";
import { httpProxy } from "utils/proxy/http";
const proxyName = "omadaProxyHandler";
const sessionCacheKey = `${proxyName}__session`;
const logger = createLogger(proxyName);
async function login(loginUrl, username, password, controllerVersionMajor) {
function getSessionCacheId(group, service, index) {
return [sessionCacheKey, group, service, index ?? "0"].join(".");
}
function shouldRetryWithFreshSession(status, responseData, attempt, usedCachedSession) {
return attempt === 0 && usedCachedSession && (status === 401 || status === 403 || responseData?.errorCode > 0);
}
function getCookieHeader(responseHeaders) {
const setCookie = responseHeaders?.["set-cookie"];
if (!setCookie) return null;
const cookies = new Map();
(Array.isArray(setCookie) ? setCookie : [setCookie]).forEach((cookie) => {
const cookiePair = cookie.split(";")[0];
if (!cookiePair) return;
const separatorIndex = cookiePair.indexOf("=");
const cookieName = separatorIndex === -1 ? cookiePair : cookiePair.slice(0, separatorIndex);
cookies.set(cookieName, cookiePair);
});
return cookies.size > 0 ? Array.from(cookies.values()).join("; ") : null;
}
async function login(loginUrl, username, password, controllerVersionMajor, sessionCacheId) {
const params = {
username,
password,
@@ -20,7 +48,7 @@ async function login(loginUrl, username, password, controllerVersionMajor) {
};
}
const [status, contentType, data] = await httpProxy(loginUrl, {
const [status, contentType, data, responseHeaders] = await httpProxy(loginUrl, {
method: "POST",
body: JSON.stringify(params),
headers: {
@@ -28,7 +56,20 @@ async function login(loginUrl, username, password, controllerVersionMajor) {
},
});
return [status, JSON.parse(data.toString())];
const loginResponseData = JSON.parse(data.toString());
if (status === 200 && loginResponseData.errorCode === 0) {
cache.put(
sessionCacheId,
{
token: loginResponseData.result.token,
cookieHeader: getCookieHeader(responseHeaders),
},
55 * 60 * 1000, // Cache session for 55 minutes
);
}
return [status, loginResponseData];
}
export default async function omadaProxyHandler(req, res) {
@@ -86,182 +127,247 @@ export default async function omadaProxyHandler(req, res) {
break;
}
const [loginStatus, loginResponseData] = await login(
loginUrl,
widget.username,
widget.password,
controllerVersionMajor,
);
const sessionCacheId = getSessionCacheId(group, service, index);
let session = cache.get(sessionCacheId);
if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
return res
.status(loginStatus)
.json({ error: { message: "Error logging in to Omada controller", url: loginUrl, data: loginResponseData } });
}
for (let attempt = 0; attempt < 2; attempt += 1) {
try {
const usedCachedSession = Boolean(session);
const { token } = loginResponseData.result;
if (!session) {
const [loginStatus, loginResponseData] = await login(
loginUrl,
widget.username,
widget.password,
controllerVersionMajor,
sessionCacheId,
);
let sitesUrl;
let body = {};
let params = { token };
let headers = { "Csrf-Token": token };
let method = "GET";
if (loginStatus !== 200 || loginResponseData.errorCode > 0) {
return res.status(loginStatus).json({
error: { message: "Error logging in to Omada controller", url: loginUrl, data: loginResponseData },
});
}
switch (controllerVersionMajor) {
case 3:
sitesUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
body = {
method: "getUserSites",
params: {
userName: widget.username,
},
};
method = "POST";
break;
case 4:
sitesUrl = `${url}/api/v2/sites?token=${token}&currentPage=1&currentPageSize=1000`;
break;
case 5:
case 6:
sitesUrl = `${url}/${cId}/api/v2/sites?token=${token}&currentPage=1&currentPageSize=1000`;
break;
default:
break;
}
session = cache.get(sessionCacheId);
}
[status, contentType, data] = await httpProxy(sitesUrl, {
method,
params,
body: JSON.stringify(body),
headers,
});
const { token, cookieHeader } = session;
const sitesResponseData = JSON.parse(data);
let sitesUrl;
let body = {};
let params = { token };
let headers = { "Csrf-Token": token };
if (cookieHeader) {
headers.Cookie = cookieHeader;
}
let method = "GET";
if (status !== 200 || sitesResponseData.errorCode > 0) {
logger.debug(`HTTP ${status} getting sites list: ${sitesResponseData.msg}`);
return res
.status(status)
.json({ error: { message: "Error getting sites list", url, data: sitesResponseData } });
}
switch (controllerVersionMajor) {
case 3:
sitesUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
body = {
method: "getUserSites",
params: {
userName: widget.username,
},
};
headers = { "Content-Type": "application/json" };
if (cookieHeader) {
headers.Cookie = cookieHeader;
}
method = "POST";
break;
case 4:
sitesUrl = `${url}/api/v2/sites?token=${token}&currentPage=1&currentPageSize=1000`;
break;
case 5:
case 6:
sitesUrl = `${url}/${cId}/api/v2/sites?token=${token}&currentPage=1&currentPageSize=1000`;
break;
default:
break;
}
const site =
controllerVersionMajor === 3
? sitesResponseData.result.siteList.find((s) => s.name === widget.site)
: sitesResponseData.result.data.find((s) => s.name === widget.site);
if (!site) {
return res.status(status).json({ error: { message: `Site ${widget.site} is not found`, url: sitesUrl, data } });
}
let siteResponseData;
let connectedAp;
let activeUser;
let connectedSwitches;
let connectedGateways;
let alerts;
if (controllerVersionMajor === 3) {
// Omada v3 controller requires switching site
const switchUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
method = "POST";
body = {
method: "switchSite",
params: {
siteName: site.siteName,
userName: widget.username,
},
};
headers = { "Content-Type": "application/json" };
params = { token };
[status, contentType, data] = await httpProxy(switchUrl, {
method,
params,
body: JSON.stringify(body),
headers,
});
const switchResponseData = JSON.parse(data);
if (status !== 200 || switchResponseData.errorCode > 0) {
logger.error(`HTTP ${status} getting sites list: ${data}`);
return res.status(status).json({ error: { message: "Error switching site", url: switchUrl, data } });
}
const statsUrl = `${url}/web/v1/controller?getGlobalStat=&token=${token}`;
[status, contentType, data] = await httpProxy(statsUrl, {
method,
params,
body: JSON.stringify({
method: "getGlobalStat",
}),
headers,
});
siteResponseData = JSON.parse(data);
if (status !== 200 || siteResponseData.errorCode > 0) {
return res.status(status).json({ error: { message: "Error getting stats", url: statsUrl, data } });
}
connectedAp = siteResponseData.result.connectedAp;
activeUser = siteResponseData.result.activeUser;
alerts = siteResponseData.result.alerts;
} else if ([4, 5, 6].includes(controllerVersionMajor)) {
const siteName = controllerVersionMajor > 4 ? site.id : site.key;
const siteStatsUrl =
controllerVersionMajor === 4
? `${url}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}&currentPage=1&currentPageSize=1000`
: `${url}/${cId}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}&currentPage=1&currentPageSize=1000`;
[status, contentType, data] = await httpProxy(siteStatsUrl, {
headers: {
"Csrf-Token": token,
},
});
siteResponseData = JSON.parse(data);
if (status !== 200 || siteResponseData.errorCode > 0) {
logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`);
return res.status(status === 200 ? 500 : status).json({
error: {
message: "Error getting stats",
url: siteStatsUrl,
data: siteResponseData,
},
[status, contentType, data] = await httpProxy(sitesUrl, {
method,
params,
body: JSON.stringify(body),
headers: { ...headers },
});
const sitesResponseData = JSON.parse(data);
if (status !== 200 || sitesResponseData.errorCode > 0) {
logger.debug(`HTTP ${status} getting sites list: ${sitesResponseData.msg}`);
if (shouldRetryWithFreshSession(status, sitesResponseData, attempt, usedCachedSession)) {
cache.del(sessionCacheId);
session = null;
continue;
}
return res
.status(status)
.json({ error: { message: "Error getting sites list", url, data: sitesResponseData } });
}
const site =
controllerVersionMajor === 3
? sitesResponseData.result.siteList.find((s) => s.name === widget.site)
: sitesResponseData.result.data.find((s) => s.name === widget.site);
if (!site) {
return res
.status(status)
.json({ error: { message: `Site ${widget.site} is not found`, url: sitesUrl, data } });
}
let siteResponseData;
let connectedAp;
let activeUser;
let connectedSwitches;
let connectedGateways;
let alerts;
if (controllerVersionMajor === 3) {
// Omada v3 controller requires switching site
const switchUrl = `${url}/web/v1/controller?ajax=&token=${token}`;
method = "POST";
body = {
method: "switchSite",
params: {
siteName: site.siteName,
userName: widget.username,
},
};
headers = { "Content-Type": "application/json" };
if (cookieHeader) {
headers.Cookie = cookieHeader;
}
params = { token };
[status, contentType, data] = await httpProxy(switchUrl, {
method,
params,
body: JSON.stringify(body),
headers: { ...headers },
});
const switchResponseData = JSON.parse(data);
if (status !== 200 || switchResponseData.errorCode > 0) {
logger.error(`HTTP ${status} getting sites list: ${data}`);
if (shouldRetryWithFreshSession(status, switchResponseData, attempt, usedCachedSession)) {
cache.del(sessionCacheId);
session = null;
continue;
}
return res.status(status).json({ error: { message: "Error switching site", url: switchUrl, data } });
}
const statsUrl = `${url}/web/v1/controller?getGlobalStat=&token=${token}`;
[status, contentType, data] = await httpProxy(statsUrl, {
method,
params,
body: JSON.stringify({
method: "getGlobalStat",
}),
headers: { ...headers },
});
siteResponseData = JSON.parse(data);
if (status !== 200 || siteResponseData.errorCode > 0) {
if (shouldRetryWithFreshSession(status, siteResponseData, attempt, usedCachedSession)) {
cache.del(sessionCacheId);
session = null;
continue;
}
return res.status(status).json({ error: { message: "Error getting stats", url: statsUrl, data } });
}
connectedAp = siteResponseData.result.connectedAp;
activeUser = siteResponseData.result.activeUser;
alerts = siteResponseData.result.alerts;
} else if ([4, 5, 6].includes(controllerVersionMajor)) {
const siteName = controllerVersionMajor > 4 ? site.id : site.key;
const siteStatsUrl =
controllerVersionMajor === 4
? `${url}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}&currentPage=1&currentPageSize=1000`
: `${url}/${cId}/api/v2/sites/${siteName}/dashboard/overviewDiagram?token=${token}&currentPage=1&currentPageSize=1000`;
[status, contentType, data] = await httpProxy(siteStatsUrl, {
headers: { ...headers },
});
siteResponseData = JSON.parse(data);
if (status !== 200 || siteResponseData.errorCode > 0) {
logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`);
if (shouldRetryWithFreshSession(status, siteResponseData, attempt, usedCachedSession)) {
cache.del(sessionCacheId);
session = null;
continue;
}
return res.status(status === 200 ? 500 : status).json({
error: {
message: "Error getting stats",
url: siteStatsUrl,
data: siteResponseData,
},
});
}
const alertUrl =
controllerVersionMajor === 4
? `${url}/api/v2/sites/${siteName}/alerts/num?token=${token}&currentPage=1&currentPageSize=1000`
: `${url}/${cId}/api/v2/sites/${siteName}/alerts/num?token=${token}&currentPage=1&currentPageSize=1000`;
[status, contentType, data] = await httpProxy(alertUrl, {
headers: { ...headers },
});
const alertResponseData = JSON.parse(data);
if (status !== 200 || alertResponseData.errorCode > 0) {
if (shouldRetryWithFreshSession(status, alertResponseData, attempt, usedCachedSession)) {
cache.del(sessionCacheId);
session = null;
continue;
}
return res.status(status === 200 ? 500 : status).json({
error: {
message: "Error getting alerts",
url: alertUrl,
data: alertResponseData,
},
});
}
activeUser = siteResponseData.result.totalClientNum;
connectedAp = siteResponseData.result.connectedApNum;
connectedGateways = siteResponseData.result.connectedGatewayNum;
connectedSwitches = siteResponseData.result.connectedSwitchNum;
alerts = alertResponseData.result.alertNum;
}
return res.send(
JSON.stringify({
connectedAp,
activeUser,
alerts,
connectedGateways,
connectedSwitches,
}),
);
} catch (error) {
if (error instanceof SyntaxError && attempt === 0) {
cache.del(sessionCacheId);
session = null;
continue;
}
throw error;
}
const alertUrl =
controllerVersionMajor === 4
? `${url}/api/v2/sites/${siteName}/alerts/num?token=${token}&currentPage=1&currentPageSize=1000`
: `${url}/${cId}/api/v2/sites/${siteName}/alerts/num?token=${token}&currentPage=1&currentPageSize=1000`;
[status, contentType, data] = await httpProxy(alertUrl, {
headers: {
"Csrf-Token": token,
},
});
const alertResponseData = JSON.parse(data);
activeUser = siteResponseData.result.totalClientNum;
connectedAp = siteResponseData.result.connectedApNum;
connectedGateways = siteResponseData.result.connectedGatewayNum;
connectedSwitches = siteResponseData.result.connectedSwitchNum;
alerts = alertResponseData.result.alertNum;
}
return res.send(
JSON.stringify({
connectedAp,
activeUser,
alerts,
connectedGateways,
connectedSwitches,
}),
);
}
}

View File

@@ -2,14 +2,24 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import createMockRes from "test-utils/create-mock-res";
const { httpProxy, getServiceWidget, logger } = vi.hoisted(() => ({
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
logger: {
debug: vi.fn(),
error: vi.fn(),
},
}));
const { httpProxy, getServiceWidget, cache, logger } = vi.hoisted(() => {
const store = new Map();
return {
httpProxy: vi.fn(),
getServiceWidget: vi.fn(),
cache: {
get: vi.fn((k) => store.get(k)),
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("utils/logger", () => ({
default: () => logger,
@@ -20,15 +30,19 @@ vi.mock("utils/config/service-helpers", () => ({
vi.mock("utils/proxy/http", () => ({
httpProxy,
}));
vi.mock("memory-cache", () => ({
default: cache,
...cache,
}));
import omadaProxyHandler from "./proxy";
describe("widgets/omada/proxy", () => {
beforeEach(() => {
vi.clearAllMocks();
// Clear one-off implementations between tests (some branches return early).
httpProxy.mockReset();
getServiceWidget.mockReset();
cache._reset();
});
it("fetches controller info, logs in, selects site, and returns overview stats (v4)", async () => {
@@ -51,6 +65,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
// sites list
.mockResolvedValueOnce([
@@ -91,6 +106,12 @@ describe("widgets/omada/proxy", () => {
connectedSwitches: 3,
}),
);
expect(httpProxy.mock.calls[2][1]).toMatchObject({
headers: {
"Csrf-Token": "t",
Cookie: "TPOMADA_SESSIONID=sid",
},
});
});
it("returns an error when controller info cannot be retrieved", async () => {
@@ -169,6 +190,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 2, msg: "bad" })]);
@@ -195,6 +217,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
@@ -222,6 +245,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
// getUserSites
.mockResolvedValueOnce([
@@ -271,6 +295,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
@@ -301,6 +326,7 @@ describe("widgets/omada/proxy", () => {
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
@@ -324,4 +350,414 @@ describe("widgets/omada/proxy", () => {
},
});
});
it("reuses a cached Omada session across polls", async () => {
getServiceWidget.mockResolvedValue({
url: "http://omada",
username: "u",
password: "p",
site: "Default",
});
httpProxy
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
])
.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({
errorCode: 0,
result: {
totalClientNum: 10,
connectedApNum: 2,
connectedGatewayNum: 1,
connectedSwitchNum: 3,
},
}),
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({
errorCode: 0,
result: {
totalClientNum: 10,
connectedApNum: 2,
connectedGatewayNum: 1,
connectedSwitchNum: 3,
},
}),
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })]);
const req = { query: { group: "g", service: "svc", index: "0" } };
await omadaProxyHandler(req, createMockRes());
await omadaProxyHandler(req, createMockRes());
const loginCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("/api/v2/login"));
expect(loginCalls).toHaveLength(1);
expect(httpProxy.mock.calls[6][1].headers.Cookie).toBe("TPOMADA_SESSIONID=sid");
});
it("does not reuse a cached session across different widget identities", async () => {
getServiceWidget.mockResolvedValue({
url: "http://omada",
username: "u",
password: "p",
site: "Default",
});
httpProxy
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
])
.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t1" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid1; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({
errorCode: 0,
result: {
totalClientNum: 10,
connectedApNum: 2,
connectedGatewayNum: 1,
connectedSwitchNum: 3,
},
}),
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
])
.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t2" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid2; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({
errorCode: 0,
result: {
totalClientNum: 10,
connectedApNum: 2,
connectedGatewayNum: 1,
connectedSwitchNum: 3,
},
}),
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })]);
await omadaProxyHandler({ query: { group: "g1", service: "svc", index: "0" } }, createMockRes());
await omadaProxyHandler({ query: { group: "g2", service: "svc", index: "0" } }, createMockRes());
const loginCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("/api/v2/login"));
expect(loginCalls).toHaveLength(2);
expect(httpProxy.mock.calls[2][1].headers.Cookie).toBe("TPOMADA_SESSIONID=sid1");
expect(httpProxy.mock.calls[7][1].headers.Cookie).toBe("TPOMADA_SESSIONID=sid2");
});
it("keeps the latest value when Omada sets the same cookie more than once during login", async () => {
getServiceWidget.mockResolvedValue({
url: "http://omada",
username: "u",
password: "p",
site: "Default",
});
httpProxy
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
])
.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{
"set-cookie": [
"TPOMADA_SESSIONID=deleteMe; Path=/; Max-Age=0",
"TPOMADA_SESSIONID=sid; Path=/; HttpOnly",
"rememberMe=deleteMe; Path=/; Max-Age=0",
],
},
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({
errorCode: 0,
result: {
totalClientNum: 10,
connectedApNum: 2,
connectedGatewayNum: 1,
connectedSwitchNum: 3,
},
}),
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await omadaProxyHandler(req, res);
expect(httpProxy.mock.calls[2][1].headers.Cookie).toBe("TPOMADA_SESSIONID=sid; rememberMe=deleteMe");
expect(res.body).toBe(
JSON.stringify({
connectedAp: 2,
activeUser: 10,
alerts: 4,
connectedGateways: 1,
connectedSwitches: 3,
}),
);
});
it("does not reuse a mutated content-length header on later GET requests", async () => {
getServiceWidget.mockResolvedValue({
url: "http://omada",
username: "u",
password: "p",
site: "Default",
});
const responses = [
[200, "application/json", JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } })],
[
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "t" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=sid; Path=/; HttpOnly"] },
],
[
200,
"application/json",
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
],
[
200,
"application/json",
JSON.stringify({
errorCode: 0,
result: {
totalClientNum: 10,
connectedApNum: 2,
connectedGatewayNum: 1,
connectedSwitchNum: 3,
},
}),
],
[200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })],
];
httpProxy.mockImplementation(async (_url, params = {}) => {
if (params.body) {
params.headers["content-length"] = Buffer.byteLength(params.body);
}
return responses.shift();
});
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await omadaProxyHandler(req, res);
expect(httpProxy.mock.calls[2][1].headers["content-length"]).toBe(2);
expect(httpProxy.mock.calls[3][1].headers["content-length"]).toBeUndefined();
expect(httpProxy.mock.calls[4][1].headers["content-length"]).toBeUndefined();
expect(res.body).toBe(
JSON.stringify({
connectedAp: 2,
activeUser: 10,
alerts: 4,
connectedGateways: 1,
connectedSwitches: 3,
}),
);
});
it("clears the cached session and re-authenticates when an authenticated response is not JSON", async () => {
cache.put("omadaProxyHandler__session.g.svc.0", {
token: "stale-token",
cookieHeader: "TPOMADA_SESSIONID=stale",
});
getServiceWidget.mockResolvedValue({
url: "http://omada",
username: "u",
password: "p",
site: "Default",
});
httpProxy
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
])
.mockResolvedValueOnce([200, "text/html", Buffer.from("<!DOCTYPE html>login")])
.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "fresh-token" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=fresh; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({
errorCode: 0,
result: {
totalClientNum: 10,
connectedApNum: 2,
connectedGatewayNum: 1,
connectedSwitchNum: 3,
},
}),
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await omadaProxyHandler(req, res);
expect(cache.del).toHaveBeenCalledWith("omadaProxyHandler__session.g.svc.0");
const loginCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("/api/v2/login"));
expect(loginCalls).toHaveLength(1);
expect(res.body).toBe(
JSON.stringify({
connectedAp: 2,
activeUser: 10,
alerts: 4,
connectedGateways: 1,
connectedSwitches: 3,
}),
);
});
it("clears the cached session and re-authenticates when a cached session returns a JSON auth error", async () => {
cache.put("omadaProxyHandler__session.g.svc.0", {
token: "stale-token",
cookieHeader: "TPOMADA_SESSIONID=stale",
});
getServiceWidget.mockResolvedValue({
url: "http://omada",
username: "u",
password: "p",
site: "Default",
});
httpProxy
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ result: { omadacId: "cid", controllerVer: "4.5.6" } }),
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 1, msg: "Login required" })])
.mockResolvedValueOnce([
200,
"application/json",
Buffer.from(JSON.stringify({ errorCode: 0, result: { token: "fresh-token" } })),
{ "set-cookie": ["TPOMADA_SESSIONID=fresh; Path=/; HttpOnly"] },
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({ errorCode: 0, result: { data: [{ name: "Default", key: "sitekey" }] } }),
])
.mockResolvedValueOnce([
200,
"application/json",
JSON.stringify({
errorCode: 0,
result: {
totalClientNum: 10,
connectedApNum: 2,
connectedGatewayNum: 1,
connectedSwitchNum: 3,
},
}),
])
.mockResolvedValueOnce([200, "application/json", JSON.stringify({ errorCode: 0, result: { alertNum: 4 } })]);
const req = { query: { group: "g", service: "svc", index: "0" } };
const res = createMockRes();
await omadaProxyHandler(req, res);
expect(cache.del).toHaveBeenCalledWith("omadaProxyHandler__session.g.svc.0");
const loginCalls = httpProxy.mock.calls.filter(([url]) => url.toString().includes("/api/v2/login"));
expect(loginCalls).toHaveLength(1);
expect(res.body).toBe(
JSON.stringify({
connectedAp: 2,
activeUser: 10,
alerts: 4,
connectedGateways: 1,
connectedSwitches: 3,
}),
);
});
});

View File

@@ -9,7 +9,7 @@ const widget = {
endpoint: "org/{org}/sites",
},
resources: {
endpoint: "org/{org}/resources",
endpoint: "org/{org}/resources?pageSize=200",
},
},
};