diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml
index 1e67b0b92..2d2fab65c 100644
--- a/.github/release-drafter.yml
+++ b/.github/release-drafter.yml
@@ -10,6 +10,7 @@ version-resolver:
- 'breaking-change'
minor:
labels:
+ - 'minor'
- 'enhancement'
- 'feature'
patch:
diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml
index 7b2bba31f..bb06499c9 100644
--- a/.github/workflows/crowdin.yml
+++ b/.github/workflows/crowdin.yml
@@ -10,6 +10,10 @@ on:
]
branches: [ dev ]
+permissions:
+ contents: write
+ pull-requests: write
+
jobs:
synchronize-with-crowdin:
name: Crowdin Sync
@@ -19,7 +23,7 @@ jobs:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: crowdin action
- uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2
+ uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2
with:
upload_translations: false
download_translations: true
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index ac351d2e3..21740a3e4 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -17,6 +17,8 @@ on:
env:
IMAGE_NAME: ${{ github.repository }}
+permissions: {}
+
jobs:
@@ -27,7 +29,6 @@ jobs:
permissions:
contents: read
packages: write
- id-token: write
steps:
- name: Checkout repository
@@ -101,7 +102,7 @@ jobs:
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Setup Docker buildx
- uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
+ uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4
- name: Build and push Docker image
id: build-and-push
diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml
index d062b6125..1698d2a3c 100644
--- a/.github/workflows/docs-publish.yml
+++ b/.github/workflows/docs-publish.yml
@@ -10,8 +10,6 @@ on:
permissions:
contents: read
- pages: write
- id-token: write
jobs:
@@ -25,7 +23,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Install uv
- uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
- run: sudo apt-get install pngquant
- name: Test Docs Build
run: uv run --frozen zensical build --clean
@@ -33,6 +31,10 @@ jobs:
name: Build & Deploy Docs
if: github.repository == 'gethomepage/homepage' && github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ pages: write
+ id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
@@ -43,7 +45,7 @@ jobs:
with:
python-version-file: ".python-version"
- name: Install uv
- uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
- run: sudo apt-get install pngquant
- name: Build Docs
run: uv run --frozen zensical build --clean
diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml
index 3baf335d0..8dc01d944 100644
--- a/.github/workflows/issue-triage.yml
+++ b/.github/workflows/issue-triage.yml
@@ -6,15 +6,15 @@ on:
discussion:
types: [labeled]
-permissions:
- discussions: write
- issues: write
+permissions: {}
jobs:
close-needs-discussion:
name: Issues Need Discussion
if: github.event_name == 'issues' && github.event.label.name == 'needs-discussion'
runs-on: ubuntu-latest
+ permissions:
+ issues: write
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
@@ -49,6 +49,8 @@ jobs:
name: Discussions Need Information
if: github.event_name == 'discussion' && github.event.label.name == 'needs-information'
runs-on: ubuntu-latest
+ permissions:
+ discussions: write
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 0d4e95132..2c731055a 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -6,6 +6,9 @@ on:
workflow_dispatch:
merge_group:
+permissions:
+ contents: read
+
jobs:
lint:
name: Linting Checks
diff --git a/.github/workflows/pr-quality.yml b/.github/workflows/pr-quality.yml
index d1931a762..751b802b9 100644
--- a/.github/workflows/pr-quality.yml
+++ b/.github/workflows/pr-quality.yml
@@ -2,7 +2,6 @@ name: PR Quality
permissions:
contents: read
- issues: read
pull-requests: write
on:
diff --git a/.github/workflows/pr-triage.yml b/.github/workflows/pr-triage.yml
index ee659cc2c..93278a061 100644
--- a/.github/workflows/pr-triage.yml
+++ b/.github/workflows/pr-triage.yml
@@ -5,7 +5,6 @@ on:
types: [labeled]
permissions:
- issues: write
pull-requests: write
jobs:
diff --git a/.github/workflows/repo-maintenance.yml b/.github/workflows/repo-maintenance.yml
index 330fc0152..50030aa3b 100644
--- a/.github/workflows/repo-maintenance.yml
+++ b/.github/workflows/repo-maintenance.yml
@@ -5,10 +5,7 @@ on:
- cron: '0 3 * * *'
workflow_dispatch:
-permissions:
- issues: write
- pull-requests: write
- discussions: write
+permissions: {}
concurrency:
group: lock
@@ -17,6 +14,9 @@ jobs:
stale:
name: 'Stale'
runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: write
steps:
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10
with:
@@ -31,6 +31,10 @@ jobs:
lock-threads:
name: 'Lock Old Threads'
runs-on: ubuntu-latest
+ permissions:
+ discussions: write
+ issues: write
+ pull-requests: write
steps:
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
with:
@@ -56,6 +60,8 @@ jobs:
close-answered-discussions:
name: 'Close Answered Discussions'
runs-on: ubuntu-latest
+ permissions:
+ discussions: write
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
@@ -112,6 +118,8 @@ jobs:
close-outdated-discussions:
name: 'Close Outdated Discussions'
runs-on: ubuntu-latest
+ permissions:
+ discussions: write
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
@@ -203,6 +211,8 @@ jobs:
close-unsupported-feature-requests:
name: 'Close Unsupported Feature Requests'
runs-on: ubuntu-latest
+ permissions:
+ discussions: write
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 887c68b51..8c24d6a26 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,6 +5,9 @@ on:
push:
workflow_dispatch:
+permissions:
+ contents: read
+
jobs:
vitest:
runs-on: ubuntu-latest
diff --git a/docs/widgets/authoring/getting-started.md b/docs/widgets/authoring/getting-started.md
index d93b01862..27fbb0570 100644
--- a/docs/widgets/authoring/getting-started.md
+++ b/docs/widgets/authoring/getting-started.md
@@ -85,6 +85,7 @@ To ensure cohesiveness of various widgets, the following should be used as a gui
- Note that we reserve the right to decline widgets for projects that are very young (eg < ~1y) or those with a small reach (eg low GitHub stars). Again, this is in an effort to keep overall widget maintenance under control.
- Widgets should be only one row of blocks
- Widgets should be no more than 4 blocks wide and generally conform to the styling / design choices of other widgets
+- Use `withWidgetFields(service, defaultFields)` from `utils/widget-fields` to apply default fields and cap configured fields without mutating the service. The helper limits widgets to 4 fields.
- Minimize the number of API calls
- Avoid the use of custom proxy unless absolutely necessary
- Widgets should be 'read-only', as in they should not make write changes using the relevant tool's API. Homepage widgets are designed to surface information, not to be a (usually worse) replacement for the tool itself.
diff --git a/docs/widgets/services/crowdsec.md b/docs/widgets/services/crowdsec.md
index 977b0adb4..fdf03a77e 100644
--- a/docs/widgets/services/crowdsec.md
+++ b/docs/widgets/services/crowdsec.md
@@ -9,7 +9,9 @@ See the [crowdsec docs](https://docs.crowdsec.net/docs/local_api/intro/#machines
in most instances you can use the default credentials (`/etc/crowdsec/local_api_credentials.yaml`).
!!! note
-Without the `limit24h` option, the widget will fetch all alerts which is limited to 100 by the API to avoid performance issues.
+
+ `alerts` counts alerts raised by your own engine in the last 24 hours. Alerts originating from the
+ CrowdSec community blocklist are excluded.
Allowed fields: `["alerts", "bans"]`.
@@ -19,5 +21,4 @@ widget:
url: http://crowdsechostorip:port
username: localhost # machine_id in crowdsec
password: password
- limit24h: true # optional, limits alerts to last 24h. Default: false
```
diff --git a/docs/widgets/services/proxmoxbackupserver.md b/docs/widgets/services/proxmoxbackupserver.md
index e3281337c..e48c56cc2 100644
--- a/docs/widgets/services/proxmoxbackupserver.md
+++ b/docs/widgets/services/proxmoxbackupserver.md
@@ -15,5 +15,5 @@ widget:
url: https://proxmoxbackupserver.host:port
username: api_token_id
password: api_token_secret
- datastore: datastore_name #optional; if ommitted, will display a combination of all datastores used / total
+ datastore: datastore_name #optional; if omitted, will display a combination of all datastores used / total
```
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 929077cfb..5e046287e 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,30 +1,13 @@
-import path from "node:path";
-import { fileURLToPath } from "node:url";
-
-import { fixupConfigRules } from "@eslint/compat";
-import { FlatCompat } from "@eslint/eslintrc";
-import js from "@eslint/js";
-import prettier from "eslint-plugin-prettier";
+import nextCoreWebVitals from "eslint-config-next/core-web-vitals";
+import prettierConfig from "eslint-config-prettier/flat";
import { defineConfig, globalIgnores } from "eslint/config";
-const __filename = fileURLToPath(import.meta.url);
-const __dirname = path.dirname(__filename);
-const compat = new FlatCompat({
- baseDirectory: __dirname,
- recommendedConfig: js.configs.recommended,
- allConfig: js.configs.all,
-});
-
export default defineConfig([
+ ...nextCoreWebVitals,
+ prettierConfig,
{
files: ["**/*.{js,mjs,cjs,jsx}"],
- extends: fixupConfigRules(compat.extends("next/core-web-vitals", "prettier", "plugin:react-hooks/recommended")),
-
- plugins: {
- prettier,
- },
-
languageOptions: {
ecmaVersion: 6,
sourceType: "module",
@@ -65,24 +48,6 @@ export default defineConfig([
allowElseIf: true,
},
],
-
- // Keep the pre-eslint-plugin-react-hooks v6 lint policy until rules are adopted incrementally
- "react-hooks/config": "off",
- "react-hooks/error-boundaries": "off",
- "react-hooks/exhaustive-deps": "warn",
- "react-hooks/gating": "off",
- "react-hooks/globals": "off",
- "react-hooks/immutability": "off",
- "react-hooks/incompatible-library": "off",
- "react-hooks/preserve-manual-memoization": "off",
- "react-hooks/purity": "off",
- "react-hooks/refs": "off",
- "react-hooks/rules-of-hooks": "error",
- "react-hooks/set-state-in-effect": "off",
- "react-hooks/set-state-in-render": "off",
- "react-hooks/static-components": "off",
- "react-hooks/unsupported-syntax": "off",
- "react-hooks/use-memo": "off",
},
},
// Vitest tests often intentionally place imports after `vi.mock(...)` to ensure
diff --git a/package.json b/package.json
index e15277f5f..7adf00be8 100644
--- a/package.json
+++ b/package.json
@@ -23,12 +23,12 @@
"gamedig": "^5.3.3",
"i18next": "^26.3.6",
"ical.js": "^2.2.1",
- "js-yaml": "^5.3.0",
+ "js-yaml": "^5.4.0",
"json-rpc-2.0": "^1.7.1",
"luxon": "^3.7.2",
"memory-cache": "^0.2.0",
"minecraftstatuspinger": "^1.2.2",
- "next": "^16.2.12",
+ "next": "^16.3.3",
"next-auth": "^4.24.15",
"next-i18next": "^16.0.8",
"ping": "^1.0.0",
@@ -36,7 +36,7 @@
"raw-body": "^4.0.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
- "react-i18next": "^17.0.11",
+ "react-i18next": "^17.0.12",
"react-icons": "^5.6.0",
"recharts": "^3.1.2",
"swr": "^2.5.1",
@@ -48,29 +48,21 @@
"xml-js": "^1.6.11"
},
"devDependencies": {
- "@eslint/compat": "^2.1.0",
- "@eslint/eslintrc": "^3.3.3",
- "@eslint/js": "^9.39.4",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/postcss": "^4.3.3",
"@testing-library/jest-dom": "^7.0.1",
"@testing-library/react": "^16.3.0",
"@vitest/coverage-v8": "^4.1.10",
"eslint": "^9.25.1",
- "eslint-config-next": "^15.5.11",
+ "eslint-config-next": "^16.3.3",
"eslint-config-prettier": "^10.1.8",
- "eslint-plugin-import": "^2.32.0",
- "eslint-plugin-jsx-a11y": "^6.10.2",
- "eslint-plugin-prettier": "^5.5.6",
- "eslint-plugin-react": "^7.37.4",
- "eslint-plugin-react-hooks": "^7.1.1",
"jsdom": "^30.0.1",
"postcss": "^8.5.26",
"prettier": "^3.8.4",
"prettier-plugin-organize-imports": "^4.3.0",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.3.3",
- "typescript": "^5.7.3",
+ "typescript": "^6.0.3",
"vitest": "^4.1.0"
},
"optionalDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e37902703..46d5d67b7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13,7 +13,7 @@ importers:
version: 2.2.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@kubernetes/client-node':
specifier: ^2.0.0
- version: 2.0.0
+ version: 2.0.0(supports-color@7.2.0)
classnames:
specifier: ^2.5.1
version: 2.5.1
@@ -22,22 +22,22 @@ importers:
version: 6.1.1
dockerode:
specifier: ^5.0.0
- version: 5.0.1
+ version: 5.0.1(supports-color@7.2.0)
follow-redirects:
specifier: ^1.16.0
- version: 1.16.0
+ version: 1.16.0(debug@4.4.3(supports-color@7.2.0))
gamedig:
specifier: ^5.3.3
version: 5.3.3
i18next:
specifier: ^26.3.6
- version: 26.3.6(typescript@5.9.3)
+ version: 26.3.6(typescript@6.0.3)
ical.js:
specifier: ^2.2.1
version: 2.2.1
js-yaml:
- specifier: ^5.3.0
- version: 5.3.0
+ specifier: ^5.4.0
+ version: 5.4.0
json-rpc-2.0:
specifier: ^1.7.1
version: 1.7.1
@@ -51,14 +51,14 @@ importers:
specifier: ^1.2.2
version: 1.2.2
next:
- specifier: ^16.2.12
- version: 16.3.0(@babel/core@7.29.7)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ specifier: ^16.3.3
+ version: 16.3.3(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
next-auth:
specifier: ^4.24.15
- version: 4.24.15(next@16.3.0(@babel/core@7.29.7)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ version: 4.24.15(next@16.3.3(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
next-i18next:
specifier: ^16.0.8
- version: 16.0.10(@types/react@19.2.18)(i18next@26.3.6(typescript@5.9.3))(next@16.3.0(@babel/core@7.29.7)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-i18next@17.0.11(i18next@26.3.6(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3))(react@19.2.8)
+ version: 16.0.10(@types/react@19.2.18)(i18next@26.3.6(typescript@6.0.3))(next@16.3.3(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-i18next@17.0.12(i18next@26.3.6(typescript@6.0.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(react@19.2.8)
ping:
specifier: ^1.0.0
version: 1.0.0
@@ -75,8 +75,8 @@ importers:
specifier: ^19.2.8
version: 19.2.8(react@19.2.8)
react-i18next:
- specifier: ^17.0.11
- version: 17.0.11(i18next@26.3.6(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3)
+ specifier: ^17.0.12
+ version: 17.0.12(i18next@26.3.6(typescript@6.0.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3)
react-icons:
specifier: ^5.6.0
version: 5.7.0(react@19.2.8)
@@ -105,15 +105,6 @@ importers:
specifier: ^1.6.11
version: 1.6.11
devDependencies:
- '@eslint/compat':
- specifier: ^2.1.0
- version: 2.1.0(eslint@9.39.5(jiti@2.7.0))
- '@eslint/eslintrc':
- specifier: ^3.3.3
- version: 3.3.6
- '@eslint/js':
- specifier: ^9.39.4
- version: 9.39.5
'@tailwindcss/forms':
specifier: ^0.5.11
version: 0.5.11(tailwindcss@4.3.3)
@@ -131,28 +122,13 @@ importers:
version: 4.1.10(vitest@4.1.10)
eslint:
specifier: ^9.25.1
- version: 9.39.5(jiti@2.7.0)
+ version: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
eslint-config-next:
- specifier: ^15.5.11
- version: 15.5.22(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
+ specifier: ^16.3.3
+ version: 16.3.3(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
eslint-config-prettier:
specifier: ^10.1.8
- version: 10.1.8(eslint@9.39.5(jiti@2.7.0))
- eslint-plugin-import:
- specifier: ^2.32.0
- version: 2.32.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0))
- eslint-plugin-jsx-a11y:
- specifier: ^6.10.2
- version: 6.10.2(eslint@9.39.5(jiti@2.7.0))
- eslint-plugin-prettier:
- specifier: ^5.5.6
- version: 5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6)
- eslint-plugin-react:
- specifier: ^7.37.4
- version: 7.37.5(eslint@9.39.5(jiti@2.7.0))
- eslint-plugin-react-hooks:
- specifier: ^7.1.1
- version: 7.1.1(eslint@9.39.5(jiti@2.7.0))
+ version: 10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
jsdom:
specifier: ^30.0.1
version: 30.0.1
@@ -164,7 +140,7 @@ importers:
version: 3.9.6
prettier-plugin-organize-imports:
specifier: ^4.3.0
- version: 4.3.0(prettier@3.9.6)(typescript@5.9.3)
+ version: 4.3.0(prettier@3.9.6)(typescript@6.0.3)
tailwind-scrollbar:
specifier: ^4.0.2
version: 4.0.2(react@19.2.8)(tailwindcss@4.3.3)
@@ -172,8 +148,8 @@ importers:
specifier: ^4.3.3
version: 4.3.3
typescript:
- specifier: ^5.7.3
- version: 5.9.3
+ specifier: ^6.0.3
+ version: 6.0.3
vitest:
specifier: ^4.1.0
version: 4.1.10(@types/node@26.2.0)(@vitest/coverage-v8@4.1.10)(jsdom@30.0.1)(vite@8.2.1(@types/node@26.2.0)(jiti@2.7.0))
@@ -342,19 +318,16 @@ packages:
peerDependencies:
eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+ '@eslint-community/eslint-utils@4.9.1':
+ resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
+ engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
'@eslint-community/regexpp@4.12.2':
resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==}
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
- '@eslint/compat@2.1.0':
- resolution: {integrity: sha512-LgaSCymEpw7tF53xvDw9SNsraPb1IBHxpdABIOM0hW8UAlP8znrjYtuxfR58FSJ3L9BhwD+FaPRFQpZq84Nh6g==}
- engines: {node: ^20.19.0 || ^22.13.0 || >=24}
- peerDependencies:
- eslint: ^8.40 || 9 || 10
- peerDependenciesMeta:
- eslint:
- optional: true
-
'@eslint/config-array@0.21.2':
resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -367,10 +340,6 @@ packages:
resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@eslint/core@1.2.1':
- resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==}
- engines: {node: ^20.19.0 || ^22.13.0 || >=24}
-
'@eslint/eslintrc@3.3.6':
resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -670,60 +639,60 @@ packages:
'@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3
'@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3
- '@next/env@16.3.0':
- resolution: {integrity: sha512-o9r1S0BNiNreHP9Vs+Qnqd9kviDkJh8xIACY7UFZSmiGbbQRzPBBosvHzAU4TULHOIuOj/18RSsyz2qrREmIFw==}
+ '@next/env@16.3.3':
+ resolution: {integrity: sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g==}
- '@next/eslint-plugin-next@15.5.22':
- resolution: {integrity: sha512-SB8PGBmpAiVmOtMOIfObnI2VA3MN3jUGg0iYTh4MO2efxQIV/IL0U8iYOok/8DldCEKARyCOJJix+Vvgzaic/Q==}
+ '@next/eslint-plugin-next@16.3.3':
+ resolution: {integrity: sha512-pbEh30vvjKpDoTAmo1v3q2uM4JUi8QaEBpbmjWvGfoec2jLghy/WNtvzAT0bk+Ik9oz6etjt4YjXEk4BQnicCw==}
- '@next/swc-darwin-arm64@16.3.0':
- resolution: {integrity: sha512-55hpqq18bEVAlxedlTt3tFqZmKg2nUXT1kn1G/BGEy0R13h3LwtwHPVzzjG6P4LLeOHE32PFDQUVaJEWvBEZBw==}
+ '@next/swc-darwin-arm64@16.3.3':
+ resolution: {integrity: sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
- '@next/swc-darwin-x64@16.3.0':
- resolution: {integrity: sha512-SOi96kSaF5T+0wW4koiM1bWzSPwjzTesC1p3df+FjdOi5LIQkBK/blxh7HdoKnNuI4PURF1OO7TZqtfnbWDSgw==}
+ '@next/swc-darwin-x64@16.3.3':
+ resolution: {integrity: sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
- '@next/swc-linux-arm64-gnu@16.3.0':
- resolution: {integrity: sha512-P0gZAoPMF4dyTRzhmkV4PrqVzSOB6t4mC1oI3c4dqijJ+OVEVx5clIXAKR4/uQpsqw2KKM/0D5tVumcR2r5blg==}
+ '@next/swc-linux-arm64-gnu@16.3.3':
+ resolution: {integrity: sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-arm64-musl@16.3.0':
- resolution: {integrity: sha512-tXXGKJw0m37O0eKJARVTX/TheKPhz0QFVtVVZXmOig+9YKLQOSP6hvf2pxv5DO7CLEJyTHx3Pg043CDQkv1G4Q==}
+ '@next/swc-linux-arm64-musl@16.3.3':
+ resolution: {integrity: sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@next/swc-linux-x64-gnu@16.3.0':
- resolution: {integrity: sha512-pjGxK5EY7yWml78ALejFkWmgHsU7wbFQrISiugpH6FbUJhgEvw3xFZ/EBAtLl7QtL0WdQKiG9eWJ3mOKGTukHw==}
+ '@next/swc-linux-x64-gnu@16.3.3':
+ resolution: {integrity: sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@next/swc-linux-x64-musl@16.3.0':
- resolution: {integrity: sha512-sjo++Xx+lomlPs3HRsHWhVDyGG6ms1kGW5EtHLERdII8AyG1i+f6aq68xHREO6AEMlhjTNEWBSmfJfqm9orf7g==}
+ '@next/swc-linux-x64-musl@16.3.3':
+ resolution: {integrity: sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
- '@next/swc-win32-arm64-msvc@16.3.0':
- resolution: {integrity: sha512-C5JSgiO54wURdaxdEUIXqkz04uMqC9UmPX1gtDrV/5Tf1UowdWYI8uA5hfFbPolTlp0q4KZ60xlHePNibf0VIw==}
+ '@next/swc-win32-arm64-msvc@16.3.3':
+ resolution: {integrity: sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
- '@next/swc-win32-x64-msvc@16.3.0':
- resolution: {integrity: sha512-fDOggsweNb5SSw0ZKVk6U+gxSyGFFlIBY/LBc1r8GUj4u/6t6oArL+Pmkg0MBnsgR+KkdsURilVH4F3GXUGepA==}
+ '@next/swc-win32-x64-msvc@16.3.3':
+ resolution: {integrity: sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -753,10 +722,6 @@ packages:
'@panva/hkdf@1.2.1':
resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
- '@pkgr/core@0.3.6':
- resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==}
- engines: {node: ^14.18.0 || >=16.0.0}
-
'@protobufjs/aspromise@1.1.2':
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
@@ -908,9 +873,6 @@ packages:
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
- '@rushstack/eslint-patch@1.16.1':
- resolution: {integrity: sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==}
-
'@sindresorhus/is@5.6.0':
resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
engines: {node: '>=14.16'}
@@ -924,9 +886,6 @@ packages:
'@standard-schema/utils@0.3.0':
resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==}
- '@swc/helpers@0.5.15':
- resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
-
'@swc/helpers@0.5.23':
resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
@@ -1146,63 +1105,63 @@ packages:
'@types/use-sync-external-store@0.0.6':
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
- '@typescript-eslint/eslint-plugin@8.66.0':
- resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==}
+ '@typescript-eslint/eslint-plugin@8.68.0':
+ resolution: {integrity: sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- '@typescript-eslint/parser': ^8.66.0
+ '@typescript-eslint/parser': ^8.68.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/parser@8.66.0':
- resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==}
+ '@typescript-eslint/parser@8.68.0':
+ resolution: {integrity: sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/project-service@8.66.0':
- resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==}
+ '@typescript-eslint/project-service@8.68.0':
+ resolution: {integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/scope-manager@8.66.0':
- resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==}
+ '@typescript-eslint/scope-manager@8.68.0':
+ resolution: {integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.66.0':
- resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==}
+ '@typescript-eslint/tsconfig-utils@8.68.0':
+ resolution: {integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/type-utils@8.66.0':
- resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==}
+ '@typescript-eslint/type-utils@8.68.0':
+ resolution: {integrity: sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/types@8.66.0':
- resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==}
+ '@typescript-eslint/types@8.68.0':
+ resolution: {integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/typescript-estree@8.66.0':
- resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==}
+ '@typescript-eslint/typescript-estree@8.68.0':
+ resolution: {integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/utils@8.66.0':
- resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==}
+ '@typescript-eslint/utils@8.68.0':
+ resolution: {integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: '>=4.8.4 <6.1.0'
- '@typescript-eslint/visitor-keys@8.66.0':
- resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==}
+ '@typescript-eslint/visitor-keys@8.68.0':
+ resolution: {integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@unrs/resolver-binding-android-arm-eabi@1.12.2':
@@ -1540,13 +1499,8 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
- baseline-browser-mapping@2.11.12:
- resolution: {integrity: sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==}
- engines: {node: '>=6.0.0'}
- hasBin: true
-
- baseline-browser-mapping@2.11.14:
- resolution: {integrity: sha512-JyJ954WzuIR8/FFzX0o5krdSTrBAkcCSRfWSleRsIHSWV+cZe2FI1PKggVkFke1hBldRs+LRxUczzE9iPmgZww==}
+ baseline-browser-mapping@2.11.19:
+ resolution: {integrity: sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==}
engines: {node: '>=6.0.0'}
hasBin: true
@@ -1610,11 +1564,8 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
- caniuse-lite@1.0.30001807:
- resolution: {integrity: sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==}
-
- caniuse-lite@1.0.30001809:
- resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==}
+ caniuse-lite@1.0.30001810:
+ resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==}
chai@6.2.2:
resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==}
@@ -1925,10 +1876,10 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
- eslint-config-next@15.5.22:
- resolution: {integrity: sha512-HhZsB3dvBsYwQpnymZm/Ps9NVaSYiSzDSXkR02CjyOcQutCuea3cF7NxE8i8Drs2+Dgw95WUzK2CMBIHd5YbMw==}
+ eslint-config-next@16.3.3:
+ resolution: {integrity: sha512-teqtsR26tnlfXFHfVLTM/4tzEzU8DMu6GS1sddZzhfGzgd2f2ofbgDUcsk6cssSCzX6Tk6fmWifJcdANSdPJrw==}
peerDependencies:
- eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
+ eslint: '>=9.0.0'
typescript: '>=3.3.1'
peerDependenciesMeta:
typescript:
@@ -1993,26 +1944,6 @@ packages:
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
- eslint-plugin-prettier@5.5.6:
- resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==}
- engines: {node: ^14.18.0 || >=16.0.0}
- peerDependencies:
- '@types/eslint': '>=8.0.0'
- eslint: '>=8.0.0'
- eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0'
- prettier: '>=3.0.0'
- peerDependenciesMeta:
- '@types/eslint':
- optional: true
- eslint-config-prettier:
- optional: true
-
- eslint-plugin-react-hooks@5.2.0:
- resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
- engines: {node: '>=10'}
- peerDependencies:
- eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
-
eslint-plugin-react-hooks@7.1.1:
resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==}
engines: {node: '>=18'}
@@ -2092,9 +2023,6 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
- fast-diff@1.3.0:
- resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
-
fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
@@ -2243,6 +2171,10 @@ packages:
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
engines: {node: '>=18'}
+ globals@16.4.0:
+ resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==}
+ engines: {node: '>=18'}
+
globalthis@1.0.4:
resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
engines: {node: '>= 0.4'}
@@ -2549,8 +2481,8 @@ packages:
resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
hasBin: true
- js-yaml@5.3.0:
- resolution: {integrity: sha512-muutsYr+e2+d3rTgUGslq5rxbBlUy3cJ61IsHag2QNDQV+7zXWjkUpmALIajhrlLlrgRUiymj6U3zUr/TMK84Q==}
+ js-yaml@5.4.0:
+ resolution: {integrity: sha512-jE7vUJIebKzYQI5xu4co5CRBDlDEYnHrdzsxs4O2giCz4v2SbVMYKpmt1D9L38OKQAeCWmrOTRiCV93u0UkaJA==}
hasBin: true
jsdom@30.0.1:
@@ -2890,6 +2822,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ nanoid@3.3.18:
+ resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
napi-postinstall@0.3.4:
resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
@@ -2924,8 +2861,8 @@ packages:
react: '>= 18.0.0'
react-i18next: '>= 14.0.0'
- next@16.3.0:
- resolution: {integrity: sha512-NEdGOzH+08eTXMUp9UYkA99Nhi5N6Thrhc1jgFOQgfgnGK/dA2hRwBpXep+exdFQrnwlRf/3Wixyp8lLBUpE2A==}
+ next@16.3.3:
+ resolution: {integrity: sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -3113,10 +3050,6 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
- prettier-linter-helpers@1.0.1:
- resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==}
- engines: {node: '>=6.0.0'}
-
prettier-plugin-organize-imports@4.3.0:
resolution: {integrity: sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==}
peerDependencies:
@@ -3187,8 +3120,8 @@ packages:
peerDependencies:
react: ^19.2.8
- react-i18next@17.0.11:
- resolution: {integrity: sha512-cDtkXgxjuFTWUH6V+aQn1Ve5vDiUztCNPWW5GtSHDccsgRXO1nE6QFWCEmc1KAutrb3OUv87wFShJL5RhUwPXg==}
+ react-i18next@17.0.12:
+ resolution: {integrity: sha512-lFWPEGkxQ6RhusdUkysFBD58VHfSSzvHBzqMgN0SvfVpdQGfwtNkStTqdy08/sJd7s807qqutgx93fRpD0DJ3Q==}
peerDependencies:
i18next: '>= 26.2.0'
react: '>= 16.8.0'
@@ -3561,10 +3494,6 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
- synckit@0.11.13:
- resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==}
- engines: {node: ^14.18.0 || >=16.0.0}
-
systeminformation@5.33.1:
resolution: {integrity: sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w==}
engines: {node: '>=10.0.0'}
@@ -3692,8 +3621,15 @@ packages:
resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==}
engines: {node: '>= 0.4'}
- typescript@5.9.3:
- resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ typescript-eslint@8.68.0:
+ resolution: {integrity: sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==}
+ engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ peerDependencies:
+ eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
+ typescript: '>=4.8.4 <6.1.0'
+
+ typescript@6.0.3:
+ resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==}
engines: {node: '>=14.17'}
hasBin: true
@@ -3989,20 +3925,20 @@ snapshots:
'@babel/compat-data@7.29.7': {}
- '@babel/core@7.29.7':
+ '@babel/core@7.29.7(supports-color@7.2.0)':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8
'@babel/helper-compilation-targets': 7.29.7
- '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7)
+ '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)
'@babel/helpers': 7.29.7
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
- '@babel/traverse': 7.29.8
+ '@babel/traverse': 7.29.8(supports-color@7.2.0)
'@babel/types': 7.29.8
'@jridgewell/remapping': 2.3.5
convert-source-map: 2.0.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@7.2.0)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -4027,19 +3963,19 @@ snapshots:
'@babel/helper-globals@7.29.7': {}
- '@babel/helper-module-imports@7.29.7':
+ '@babel/helper-module-imports@7.29.7(supports-color@7.2.0)':
dependencies:
- '@babel/traverse': 7.29.8
+ '@babel/traverse': 7.29.8(supports-color@7.2.0)
'@babel/types': 7.29.8
transitivePeerDependencies:
- supports-color
- '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
+ '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0)':
dependencies:
- '@babel/core': 7.29.7
- '@babel/helper-module-imports': 7.29.7
+ '@babel/core': 7.29.7(supports-color@7.2.0)
+ '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0)
'@babel/helper-validator-identifier': 7.29.7
- '@babel/traverse': 7.29.8
+ '@babel/traverse': 7.29.8(supports-color@7.2.0)
transitivePeerDependencies:
- supports-color
@@ -4066,7 +4002,7 @@ snapshots:
'@babel/parser': 7.29.8
'@babel/types': 7.29.8
- '@babel/traverse@7.29.8':
+ '@babel/traverse@7.29.8(supports-color@7.2.0)':
dependencies:
'@babel/code-frame': 7.29.7
'@babel/generator': 7.29.8
@@ -4074,7 +4010,7 @@ snapshots:
'@babel/parser': 7.29.8
'@babel/template': 7.29.7
'@babel/types': 7.29.8
- debug: 4.4.3
+ debug: 4.4.3(supports-color@7.2.0)
transitivePeerDependencies:
- supports-color
@@ -4144,23 +4080,22 @@ snapshots:
tslib: 2.8.1
optional: true
- '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0))':
+ '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))':
dependencies:
- eslint: 9.39.5(jiti@2.7.0)
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
+ eslint-visitor-keys: 3.4.3
+
+ '@eslint-community/eslint-utils@4.9.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))':
+ dependencies:
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
- '@eslint/compat@2.1.0(eslint@9.39.5(jiti@2.7.0))':
- dependencies:
- '@eslint/core': 1.2.1
- optionalDependencies:
- eslint: 9.39.5(jiti@2.7.0)
-
- '@eslint/config-array@0.21.2':
+ '@eslint/config-array@0.21.2(supports-color@7.2.0)':
dependencies:
'@eslint/object-schema': 2.1.7
- debug: 4.4.3
+ debug: 4.4.3(supports-color@7.2.0)
minimatch: 3.1.5
transitivePeerDependencies:
- supports-color
@@ -4173,14 +4108,10 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
- '@eslint/core@1.2.1':
- dependencies:
- '@types/json-schema': 7.0.15
-
- '@eslint/eslintrc@3.3.6':
+ '@eslint/eslintrc@3.3.6(supports-color@7.2.0)':
dependencies:
ajv: 6.15.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@7.2.0)
espree: 10.4.0
globals: 14.0.0
ignore: 5.3.2
@@ -4420,7 +4351,7 @@ snapshots:
dependencies:
jsep: 1.4.0
- '@kubernetes/client-node@2.0.0':
+ '@kubernetes/client-node@2.0.0(supports-color@7.2.0)':
dependencies:
'@types/js-yaml': 4.0.9
'@types/node': 26.2.0
@@ -4428,12 +4359,12 @@ snapshots:
form-data: 4.0.6
hpagent: 1.2.0
isomorphic-ws: 5.0.0(ws@8.21.2)
- js-yaml: 5.3.0
+ js-yaml: 5.4.0
jsonpath-plus: 10.4.0
openid-client: 6.8.5
rfc4648: 1.5.4
socks: 2.8.9
- socks-proxy-agent: 10.1.0
+ socks-proxy-agent: 10.1.0(supports-color@7.2.0)
stream-buffers: 3.0.3
tar-fs: 3.1.3
undici: 8.10.0
@@ -4453,34 +4384,37 @@ snapshots:
'@tybys/wasm-util': 0.10.3
optional: true
- '@next/env@16.3.0': {}
+ '@next/env@16.3.3': {}
- '@next/eslint-plugin-next@15.5.22':
+ '@next/eslint-plugin-next@16.3.3(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))':
dependencies:
+ '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
fast-glob: 3.3.1
+ transitivePeerDependencies:
+ - eslint
- '@next/swc-darwin-arm64@16.3.0':
+ '@next/swc-darwin-arm64@16.3.3':
optional: true
- '@next/swc-darwin-x64@16.3.0':
+ '@next/swc-darwin-x64@16.3.3':
optional: true
- '@next/swc-linux-arm64-gnu@16.3.0':
+ '@next/swc-linux-arm64-gnu@16.3.3':
optional: true
- '@next/swc-linux-arm64-musl@16.3.0':
+ '@next/swc-linux-arm64-musl@16.3.3':
optional: true
- '@next/swc-linux-x64-gnu@16.3.0':
+ '@next/swc-linux-x64-gnu@16.3.3':
optional: true
- '@next/swc-linux-x64-musl@16.3.0':
+ '@next/swc-linux-x64-musl@16.3.3':
optional: true
- '@next/swc-win32-arm64-msvc@16.3.0':
+ '@next/swc-win32-arm64-msvc@16.3.3':
optional: true
- '@next/swc-win32-x64-msvc@16.3.0':
+ '@next/swc-win32-x64-msvc@16.3.3':
optional: true
'@nodable/entities@2.2.0': {}
@@ -4503,8 +4437,6 @@ snapshots:
'@panva/hkdf@1.2.1': {}
- '@pkgr/core@0.3.6': {}
-
'@protobufjs/aspromise@1.1.2': {}
'@protobufjs/base64@1.1.2': {}
@@ -4602,8 +4534,6 @@ snapshots:
'@rtsao/scc@1.1.0': {}
- '@rushstack/eslint-patch@1.16.1': {}
-
'@sindresorhus/is@5.6.0': {}
'@so-ric/colorspace@1.1.6':
@@ -4615,10 +4545,6 @@ snapshots:
'@standard-schema/utils@0.3.0': {}
- '@swc/helpers@0.5.15':
- dependencies:
- tslib: 2.8.1
-
'@swc/helpers@0.5.23':
dependencies:
tslib: 2.8.1
@@ -4812,95 +4738,95 @@ snapshots:
'@types/use-sync-external-store@0.0.6': {}
- '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)':
+ '@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.66.0
- '@typescript-eslint/type-utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
- '@typescript-eslint/utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
- '@typescript-eslint/visitor-keys': 8.66.0
- eslint: 9.39.5(jiti@2.7.0)
+ '@typescript-eslint/parser': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/scope-manager': 8.68.0
+ '@typescript-eslint/type-utils': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.68.0
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
ignore: 7.0.6
natural-compare: 1.4.0
- ts-api-utils: 2.5.0(typescript@5.9.3)
- typescript: 5.9.3
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)':
+ '@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/scope-manager': 8.66.0
- '@typescript-eslint/types': 8.66.0
- '@typescript-eslint/typescript-estree': 8.66.0(typescript@5.9.3)
- '@typescript-eslint/visitor-keys': 8.66.0
- debug: 4.4.3
- eslint: 9.39.5(jiti@2.7.0)
- typescript: 5.9.3
+ '@typescript-eslint/scope-manager': 8.68.0
+ '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/visitor-keys': 8.68.0
+ debug: 4.4.3(supports-color@7.2.0)
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
+ typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.66.0(typescript@5.9.3)':
+ '@typescript-eslint/project-service@8.68.0(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@5.9.3)
- '@typescript-eslint/types': 8.66.0
- debug: 4.4.3
- typescript: 5.9.3
+ '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.68.0
+ debug: 4.4.3(supports-color@7.2.0)
+ typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/scope-manager@8.66.0':
+ '@typescript-eslint/scope-manager@8.68.0':
dependencies:
- '@typescript-eslint/types': 8.66.0
- '@typescript-eslint/visitor-keys': 8.66.0
+ '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/visitor-keys': 8.68.0
- '@typescript-eslint/tsconfig-utils@8.66.0(typescript@5.9.3)':
+ '@typescript-eslint/tsconfig-utils@8.68.0(typescript@6.0.3)':
dependencies:
- typescript: 5.9.3
+ typescript: 6.0.3
- '@typescript-eslint/type-utils@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)':
+ '@typescript-eslint/type-utils@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/types': 8.66.0
- '@typescript-eslint/typescript-estree': 8.66.0(typescript@5.9.3)
- '@typescript-eslint/utils': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
- debug: 4.4.3
- eslint: 9.39.5(jiti@2.7.0)
- ts-api-utils: 2.5.0(typescript@5.9.3)
- typescript: 5.9.3
+ '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ debug: 4.4.3(supports-color@7.2.0)
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/types@8.66.0': {}
+ '@typescript-eslint/types@8.68.0': {}
- '@typescript-eslint/typescript-estree@8.66.0(typescript@5.9.3)':
+ '@typescript-eslint/typescript-estree@8.68.0(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
- '@typescript-eslint/project-service': 8.66.0(typescript@5.9.3)
- '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@5.9.3)
- '@typescript-eslint/types': 8.66.0
- '@typescript-eslint/visitor-keys': 8.66.0
- debug: 4.4.3
+ '@typescript-eslint/project-service': 8.68.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3)
+ '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/visitor-keys': 8.68.0
+ debug: 4.4.3(supports-color@7.2.0)
minimatch: 10.2.6
semver: 7.8.5
tinyglobby: 0.2.17
- ts-api-utils: 2.5.0(typescript@5.9.3)
- typescript: 5.9.3
+ ts-api-utils: 2.5.0(typescript@6.0.3)
+ typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)':
+ '@typescript-eslint/utils@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0))
- '@typescript-eslint/scope-manager': 8.66.0
- '@typescript-eslint/types': 8.66.0
- '@typescript-eslint/typescript-estree': 8.66.0(typescript@5.9.3)
- eslint: 9.39.5(jiti@2.7.0)
- typescript: 5.9.3
+ '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
+ '@typescript-eslint/scope-manager': 8.68.0
+ '@typescript-eslint/types': 8.68.0
+ '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@6.0.3)
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
+ typescript: 6.0.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/visitor-keys@8.66.0':
+ '@typescript-eslint/visitor-keys@8.68.0':
dependencies:
- '@typescript-eslint/types': 8.66.0
+ '@typescript-eslint/types': 8.68.0
eslint-visitor-keys: 5.0.1
'@unrs/resolver-binding-android-arm-eabi@1.12.2':
@@ -5207,9 +5133,7 @@ snapshots:
base64-js@1.5.1: {}
- baseline-browser-mapping@2.11.12: {}
-
- baseline-browser-mapping@2.11.14: {}
+ baseline-browser-mapping@2.11.19: {}
bcrypt-pbkdf@1.0.2:
dependencies:
@@ -5240,8 +5164,8 @@ snapshots:
browserslist@4.28.8:
dependencies:
- baseline-browser-mapping: 2.11.14
- caniuse-lite: 1.0.30001809
+ baseline-browser-mapping: 2.11.19
+ caniuse-lite: 1.0.30001810
electron-to-chromium: 1.5.406
node-releases: 2.0.53
update-browserslist-db: 1.3.1(browserslist@4.28.8)
@@ -5287,9 +5211,7 @@ snapshots:
callsites@3.1.0: {}
- caniuse-lite@1.0.30001807: {}
-
- caniuse-lite@1.0.30001809: {}
+ caniuse-lite@1.0.30001810: {}
chai@6.2.2: {}
@@ -5435,13 +5357,17 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
- debug@3.2.7:
+ debug@3.2.7(supports-color@7.2.0):
dependencies:
ms: 2.1.3
+ optionalDependencies:
+ supports-color: 7.2.0
- debug@4.4.3:
+ debug@4.4.3(supports-color@7.2.0):
dependencies:
ms: 2.1.3
+ optionalDependencies:
+ supports-color: 7.2.0
decimal.js-light@2.5.1: {}
@@ -5475,21 +5401,21 @@ snapshots:
detect-libc@2.1.2: {}
- docker-modem@5.0.7:
+ docker-modem@5.0.7(supports-color@7.2.0):
dependencies:
- debug: 4.4.3
+ debug: 4.4.3(supports-color@7.2.0)
readable-stream: 3.6.2
split-ca: 1.0.1
ssh2: 1.17.0
transitivePeerDependencies:
- supports-color
- dockerode@5.0.1:
+ dockerode@5.0.1(supports-color@7.2.0):
dependencies:
'@balena/dockerignore': 1.0.2
'@grpc/grpc-js': 1.14.4
'@grpc/proto-loader': 0.7.15
- docker-modem: 5.0.7
+ docker-modem: 5.0.7(supports-color@7.2.0)
protobufjs: 7.6.5
tar-fs: 2.1.5
transitivePeerDependencies:
@@ -5649,76 +5575,76 @@ snapshots:
escape-string-regexp@4.0.0: {}
- eslint-config-next@15.5.22(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3):
+ eslint-config-next@16.3.3(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
dependencies:
- '@next/eslint-plugin-next': 15.5.22
- '@rushstack/eslint-patch': 1.16.1
- '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
- '@typescript-eslint/parser': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
- eslint: 9.39.5(jiti@2.7.0)
- eslint-import-resolver-node: 0.3.10
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0))
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0))
- eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0))
- eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0))
- eslint-plugin-react-hooks: 5.2.0(eslint@9.39.5(jiti@2.7.0))
+ '@next/eslint-plugin-next': 16.3.3(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
+ eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
+ eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
+ eslint-plugin-react: 7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
+ eslint-plugin-react-hooks: 7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
+ globals: 16.4.0
+ typescript-eslint: 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
optionalDependencies:
- typescript: 5.9.3
+ typescript: 6.0.3
transitivePeerDependencies:
+ - '@typescript-eslint/parser'
- eslint-import-resolver-webpack
- eslint-plugin-import-x
- supports-color
- eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)):
+ eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)):
dependencies:
- eslint: 9.39.5(jiti@2.7.0)
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
- eslint-import-resolver-node@0.3.10:
+ eslint-import-resolver-node@0.3.10(supports-color@7.2.0):
dependencies:
- debug: 3.2.7
+ debug: 3.2.7(supports-color@7.2.0)
is-core-module: 2.16.2
resolve: 2.0.0-next.7
transitivePeerDependencies:
- supports-color
- eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)):
+ eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
dependencies:
'@nolyfill/is-core-module': 1.0.39
- debug: 4.4.3
- eslint: 9.39.5(jiti@2.7.0)
+ debug: 4.4.3(supports-color@7.2.0)
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
get-tsconfig: 4.14.1
is-bun-module: 2.0.0
stable-hash: 0.0.5
tinyglobby: 0.2.17
unrs-resolver: 1.12.2
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.14.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)):
+ eslint-module-utils@2.14.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
dependencies:
- debug: 3.2.7
+ debug: 3.2.7(supports-color@7.2.0)
optionalDependencies:
- '@typescript-eslint/parser': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
- eslint: 9.39.5(jiti@2.7.0)
- eslint-import-resolver-node: 0.3.10
- eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0))
+ '@typescript-eslint/parser': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
+ eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
transitivePeerDependencies:
- supports-color
- eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)):
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
dependencies:
'@rtsao/scc': 1.1.0
array-includes: 3.1.9
array.prototype.findlastindex: 1.2.6
array.prototype.flat: 1.3.3
array.prototype.flatmap: 1.3.3
- debug: 3.2.7
+ debug: 3.2.7(supports-color@7.2.0)
doctrine: 2.1.0
- eslint: 9.39.5(jiti@2.7.0)
- eslint-import-resolver-node: 0.3.10
- eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0))
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
+ eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
+ eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
hasown: 2.0.4
is-core-module: 2.16.2
is-glob: 4.0.3
@@ -5730,13 +5656,13 @@ snapshots:
string.prototype.trimend: 1.0.10
tsconfig-paths: 3.15.0
optionalDependencies:
- '@typescript-eslint/parser': 8.66.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)
+ '@typescript-eslint/parser': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
- eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@2.7.0)):
+ eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)):
dependencies:
aria-query: 5.3.2
array-includes: 3.1.9
@@ -5746,7 +5672,7 @@ snapshots:
axobject-query: 4.1.0
damerau-levenshtein: 1.0.8
emoji-regex: 9.2.2
- eslint: 9.39.5(jiti@2.7.0)
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
hasown: 2.0.4
jsx-ast-utils: 3.3.5
language-tags: 1.0.9
@@ -5755,31 +5681,18 @@ snapshots:
safe-regex-test: 1.1.0
string.prototype.includes: 2.0.1
- eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6):
+ eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
dependencies:
- eslint: 9.39.5(jiti@2.7.0)
- prettier: 3.9.6
- prettier-linter-helpers: 1.0.1
- synckit: 0.11.13
- optionalDependencies:
- eslint-config-prettier: 10.1.8(eslint@9.39.5(jiti@2.7.0))
-
- eslint-plugin-react-hooks@5.2.0(eslint@9.39.5(jiti@2.7.0)):
- dependencies:
- eslint: 9.39.5(jiti@2.7.0)
-
- eslint-plugin-react-hooks@7.1.1(eslint@9.39.5(jiti@2.7.0)):
- dependencies:
- '@babel/core': 7.29.7
+ '@babel/core': 7.29.7(supports-color@7.2.0)
'@babel/parser': 7.29.8
- eslint: 9.39.5(jiti@2.7.0)
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
hermes-parser: 0.25.1
zod: 4.4.3
zod-validation-error: 4.0.2(zod@4.4.3)
transitivePeerDependencies:
- supports-color
- eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@2.7.0)):
+ eslint-plugin-react@7.37.5(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0)):
dependencies:
array-includes: 3.1.9
array.prototype.findlast: 1.2.5
@@ -5787,7 +5700,7 @@ snapshots:
array.prototype.tosorted: 1.1.4
doctrine: 2.1.0
es-iterator-helpers: 1.4.0
- eslint: 9.39.5(jiti@2.7.0)
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
estraverse: 5.3.0
hasown: 2.0.4
jsx-ast-utils: 3.3.5
@@ -5812,14 +5725,14 @@ snapshots:
eslint-visitor-keys@5.0.1: {}
- eslint@9.39.5(jiti@2.7.0):
+ eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0):
dependencies:
- '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0))
+ '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))
'@eslint-community/regexpp': 4.12.2
- '@eslint/config-array': 0.21.2
+ '@eslint/config-array': 0.21.2(supports-color@7.2.0)
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
- '@eslint/eslintrc': 3.3.6
+ '@eslint/eslintrc': 3.3.6(supports-color@7.2.0)
'@eslint/js': 9.39.5
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.8
@@ -5829,7 +5742,7 @@ snapshots:
ajv: 6.15.0
chalk: 4.1.2
cross-spawn: 7.0.6
- debug: 4.4.3
+ debug: 4.4.3(supports-color@7.2.0)
escape-string-regexp: 4.0.0
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
@@ -5889,8 +5802,6 @@ snapshots:
fast-deep-equal@3.1.3: {}
- fast-diff@1.3.0: {}
-
fast-fifo@1.3.2: {}
fast-glob@3.3.1:
@@ -5950,7 +5861,9 @@ snapshots:
fn.name@1.1.0: {}
- follow-redirects@1.16.0: {}
+ follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)):
+ optionalDependencies:
+ debug: 4.4.3(supports-color@7.2.0)
for-each@0.3.5:
dependencies:
@@ -6053,6 +5966,8 @@ snapshots:
globals@14.0.0: {}
+ globals@16.4.0: {}
+
globalthis@1.0.4:
dependencies:
define-properties: 1.2.1
@@ -6139,9 +6054,9 @@ snapshots:
dependencies:
'@babel/runtime': 7.29.7
- i18next@26.3.6(typescript@5.9.3):
+ i18next@26.3.6(typescript@6.0.3):
optionalDependencies:
- typescript: 5.9.3
+ typescript: 6.0.3
ical.js@2.2.1: {}
@@ -6348,7 +6263,7 @@ snapshots:
dependencies:
argparse: 2.0.1
- js-yaml@5.3.0:
+ js-yaml@5.4.0:
dependencies:
argparse: 2.0.1
@@ -6625,19 +6540,21 @@ snapshots:
nanoid@3.3.17: {}
+ nanoid@3.3.18: {}
+
napi-postinstall@0.3.4: {}
natural-compare@1.4.0: {}
net@1.0.2: {}
- next-auth@4.24.15(next@16.3.0(@babel/core@7.29.7)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ next-auth@4.24.15(next@16.3.3(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies:
'@babel/runtime': 7.29.7
'@panva/hkdf': 1.2.1
cookie: 0.7.2
jose: 4.15.9
- next: 16.3.0(@babel/core@7.29.7)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ next: 16.3.3(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
oauth: 0.9.15
openid-client: 5.7.1
preact: 10.29.8(preact-render-to-string@5.2.6)
@@ -6646,37 +6563,37 @@ snapshots:
react-dom: 19.2.8(react@19.2.8)
uuid: 11.1.1
- next-i18next@16.0.10(@types/react@19.2.18)(i18next@26.3.6(typescript@5.9.3))(next@16.3.0(@babel/core@7.29.7)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-i18next@17.0.11(i18next@26.3.6(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3))(react@19.2.8):
+ next-i18next@16.0.10(@types/react@19.2.18)(i18next@26.3.6(typescript@6.0.3))(next@16.3.3(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-i18next@17.0.12(i18next@26.3.6(typescript@6.0.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3))(react@19.2.8):
dependencies:
'@types/hoist-non-react-statics': 3.3.7(@types/react@19.2.18)
hoist-non-react-statics: 3.3.2
- i18next: 26.3.6(typescript@5.9.3)
+ i18next: 26.3.6(typescript@6.0.3)
i18next-resources-to-backend: 1.2.3
- next: 16.3.0(@babel/core@7.29.7)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
+ next: 16.3.3(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
react: 19.2.8
- react-i18next: 17.0.11(i18next@26.3.6(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3)
+ react-i18next: 17.0.12(i18next@26.3.6(typescript@6.0.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3)
transitivePeerDependencies:
- '@types/react'
- next@16.3.0(@babel/core@7.29.7)(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
+ next@16.3.3(@babel/core@7.29.7(supports-color@7.2.0))(@types/node@26.2.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8):
dependencies:
- '@next/env': 16.3.0
- '@swc/helpers': 0.5.15
- baseline-browser-mapping: 2.11.12
- caniuse-lite: 1.0.30001807
+ '@next/env': 16.3.3
+ '@swc/helpers': 0.5.23
+ baseline-browser-mapping: 2.11.19
+ caniuse-lite: 1.0.30001810
postcss: 8.5.23
react: 19.2.8
react-dom: 19.2.8(react@19.2.8)
- styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.8)
+ styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.8)
optionalDependencies:
- '@next/swc-darwin-arm64': 16.3.0
- '@next/swc-darwin-x64': 16.3.0
- '@next/swc-linux-arm64-gnu': 16.3.0
- '@next/swc-linux-arm64-musl': 16.3.0
- '@next/swc-linux-x64-gnu': 16.3.0
- '@next/swc-linux-x64-musl': 16.3.0
- '@next/swc-win32-arm64-msvc': 16.3.0
- '@next/swc-win32-x64-msvc': 16.3.0
+ '@next/swc-darwin-arm64': 16.3.3
+ '@next/swc-darwin-x64': 16.3.3
+ '@next/swc-linux-arm64-gnu': 16.3.3
+ '@next/swc-linux-arm64-musl': 16.3.3
+ '@next/swc-linux-x64-gnu': 16.3.3
+ '@next/swc-linux-x64-musl': 16.3.3
+ '@next/swc-win32-arm64-msvc': 16.3.3
+ '@next/swc-win32-x64-msvc': 16.3.3
sharp: 0.35.3(@types/node@26.2.0)
transitivePeerDependencies:
- '@babel/core'
@@ -6825,7 +6742,7 @@ snapshots:
postcss@8.5.23:
dependencies:
- nanoid: 3.3.17
+ nanoid: 3.3.18
picocolors: 1.1.1
source-map-js: 1.2.1
@@ -6846,14 +6763,10 @@ snapshots:
prelude-ls@1.2.1: {}
- prettier-linter-helpers@1.0.1:
- dependencies:
- fast-diff: 1.3.0
-
- prettier-plugin-organize-imports@4.3.0(prettier@3.9.6)(typescript@5.9.3):
+ prettier-plugin-organize-imports@4.3.0(prettier@3.9.6)(typescript@6.0.3):
dependencies:
prettier: 3.9.6
- typescript: 5.9.3
+ typescript: 6.0.3
prettier@3.9.6: {}
@@ -6930,16 +6843,16 @@ snapshots:
react: 19.2.8
scheduler: 0.27.0
- react-i18next@17.0.11(i18next@26.3.6(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3):
+ react-i18next@17.0.12(i18next@26.3.6(typescript@6.0.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@6.0.3):
dependencies:
'@babel/runtime': 7.29.7
html-parse-stringify: 4.0.1
- i18next: 26.3.6(typescript@5.9.3)
+ i18next: 26.3.6(typescript@6.0.3)
react: 19.2.8
use-sync-external-store: 1.6.0(react@19.2.8)
optionalDependencies:
react-dom: 19.2.8(react@19.2.8)
- typescript: 5.9.3
+ typescript: 6.0.3
react-icons@5.7.0(react@19.2.8):
dependencies:
@@ -7238,10 +7151,10 @@ snapshots:
smart-buffer@4.2.0: {}
- socks-proxy-agent@10.1.0:
+ socks-proxy-agent@10.1.0(supports-color@7.2.0):
dependencies:
agent-base: 9.0.0
- debug: 4.4.3
+ debug: 4.4.3(supports-color@7.2.0)
socks: 2.8.9
transitivePeerDependencies:
- supports-color
@@ -7381,12 +7294,12 @@ snapshots:
dependencies:
anynum: 1.0.1
- styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.8):
+ styled-jsx@5.1.6(@babel/core@7.29.7(supports-color@7.2.0))(react@19.2.8):
dependencies:
client-only: 0.0.1
react: 19.2.8
optionalDependencies:
- '@babel/core': 7.29.7
+ '@babel/core': 7.29.7(supports-color@7.2.0)
supports-color@7.2.0:
dependencies:
@@ -7402,10 +7315,6 @@ snapshots:
symbol-tree@3.2.4: {}
- synckit@0.11.13:
- dependencies:
- '@pkgr/core': 0.3.6
-
systeminformation@5.33.1: {}
tabbable@6.5.0: {}
@@ -7514,9 +7423,9 @@ snapshots:
triple-beam@1.4.1: {}
- ts-api-utils@2.5.0(typescript@5.9.3):
+ ts-api-utils@2.5.0(typescript@6.0.3):
dependencies:
- typescript: 5.9.3
+ typescript: 6.0.3
tsconfig-paths@3.15.0:
dependencies:
@@ -7566,7 +7475,18 @@ snapshots:
possible-typed-array-names: 1.1.0
reflect.getprototypeof: 1.0.10
- typescript@5.9.3: {}
+ typescript-eslint@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3):
+ dependencies:
+ '@typescript-eslint/eslint-plugin': 8.68.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/parser': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@6.0.3)
+ '@typescript-eslint/utils': 8.68.0(eslint@9.39.5(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)
+ eslint: 9.39.5(jiti@2.7.0)(supports-color@7.2.0)
+ typescript: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ typescript@6.0.3: {}
unbox-primitive@1.1.0:
dependencies:
diff --git a/public/images/solisquad.svg b/public/images/solisquad.svg
new file mode 100644
index 000000000..b7d6b25ae
--- /dev/null
+++ b/public/images/solisquad.svg
@@ -0,0 +1 @@
+
diff --git a/public/locales/af/common.json b/public/locales/af/common.json
index 196e1f769..fd9d4c559 100644
--- a/public/locales/af/common.json
+++ b/public/locales/af/common.json
@@ -39,7 +39,9 @@
"wait": "Wag asseblief"
},
"search": {
- "placeholder": "Soek…"
+ "placeholder": "Soek…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "SVE",
diff --git a/public/locales/ar/common.json b/public/locales/ar/common.json
index 0a766c932..73ba12912 100644
--- a/public/locales/ar/common.json
+++ b/public/locales/ar/common.json
@@ -39,7 +39,9 @@
"wait": "الرجاء الإنتظار"
},
"search": {
- "placeholder": "بحث …"
+ "placeholder": "بحث …",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "المعالج",
diff --git a/public/locales/bg/common.json b/public/locales/bg/common.json
index 2d447bfe1..f221bc90f 100644
--- a/public/locales/bg/common.json
+++ b/public/locales/bg/common.json
@@ -39,7 +39,9 @@
"wait": "Моля изчакайте"
},
"search": {
- "placeholder": "Търсене…"
+ "placeholder": "Търсене…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "Процесор",
diff --git a/public/locales/ca/common.json b/public/locales/ca/common.json
index 02c2c386f..2c1c9269b 100644
--- a/public/locales/ca/common.json
+++ b/public/locales/ca/common.json
@@ -21,7 +21,7 @@
"seconds": "s"
},
"auth": {
- "signout": "Sign out"
+ "signout": "Tanca la sessió"
},
"widget": {
"missing_type": "Falta el tipus de widget: {{type}}",
@@ -39,7 +39,9 @@
"wait": "Si us plau, espereu"
},
"search": {
- "placeholder": "Cerca…"
+ "placeholder": "Cerca…",
+ "provider": "Motor de cerca: {{name}}",
+ "select_provider": "Escull motor de cerca"
},
"resources": {
"cpu": "CPU",
@@ -70,17 +72,17 @@
"empty_data": "Estat del subsistema desconegut"
},
"unifi_drive": {
- "healthy": "Healthy",
- "degraded": "Degraded",
- "no_data": "No storage data available"
+ "healthy": "Saludable",
+ "degraded": "Degradat",
+ "no_data": "Sense dades d'emmagatzematge disponible"
},
"duplicati": {
- "jobs": "Jobs",
- "stored": "Stored",
- "lastBackup": "Last Run",
- "nextRun": "Next Run",
- "running": "Running",
- "warnings": "Warnings",
+ "jobs": "Tasques",
+ "stored": "Desat",
+ "lastBackup": "Darrera execució",
+ "nextRun": "Propera execució",
+ "running": "En execució",
+ "warnings": "Avisos",
"errors": "Errors"
},
"docker": {
@@ -133,7 +135,7 @@
"series": "Sèries",
"episodes": "Episodis",
"songs": "Cançons",
- "albums": "Albums"
+ "albums": "Àlbums"
},
"esphome": {
"offline": "Desconnectat",
@@ -244,10 +246,10 @@
"seed": "Sembrat"
},
"qbittorrent": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
+ "download": "Baixada",
+ "upload": "Pujada",
+ "leech": "Sangonera",
+ "seed": "Sembrat"
},
"qnap": {
"cpuUsage": "Ús de CPU",
@@ -258,44 +260,44 @@
"invalid": "No vàlid"
},
"deluge": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
+ "download": "Baixada",
+ "upload": "Pujada",
+ "leech": "Sangonera",
+ "seed": "Sembrat"
},
"develancacheui": {
"cachehitbytes": "Bytes trobats a la memòria cau",
"cachemissbytes": "Bytes no trobats a la memòria cau"
},
"downloadstation": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
+ "download": "Baixada",
+ "upload": "Pujada",
+ "leech": "Sangonera",
+ "seed": "Sembrat"
},
"sonarr": {
"wanted": "Volgut",
"queued": "En cua",
- "series": "Series",
- "queue": "Queue",
- "unknown": "Unknown"
+ "series": "Sèries",
+ "queue": "Cua",
+ "unknown": "Desconegut"
},
"radarr": {
- "wanted": "Wanted",
+ "wanted": "Volguts",
"missing": "Falten",
- "queued": "Queued",
- "movies": "Movies",
- "queue": "Queue",
- "unknown": "Unknown"
+ "queued": "Encuat",
+ "movies": "Pel·lícules",
+ "queue": "Cua",
+ "unknown": "Desconegut"
},
"lidarr": {
- "wanted": "Wanted",
- "queued": "Queued",
+ "wanted": "Volguts",
+ "queued": "Encuat",
"artists": "Artistes"
},
"readarr": {
- "wanted": "Wanted",
- "queued": "Queued",
+ "wanted": "Volguts",
+ "queued": "Encuat",
"books": "Llibres"
},
"bazarr": {
@@ -308,16 +310,16 @@
"available": "Disponible"
},
"seerr": {
- "pending": "Pending",
- "approved": "Approved",
- "available": "Available",
- "completed": "Completed",
- "processing": "Processing",
- "issues": "Open Issues"
+ "pending": "Pendent",
+ "approved": "Aprovat",
+ "available": "Disponible",
+ "completed": "Completat",
+ "processing": "Processant",
+ "issues": "Temes oberts"
},
"netalertx": {
"total": "Total",
- "connected": "Connected",
+ "connected": "Connectat",
"new_devices": "Nous dispositius",
"down_alerts": "Alertes de caigudes"
},
@@ -328,18 +330,18 @@
"gravity": "Gravetat"
},
"adguard": {
- "queries": "Queries",
- "blocked": "Blocked",
+ "queries": "Consultes",
+ "blocked": "Bloquejat",
"filtered": "Filtrat",
"latency": "Latència"
},
"speedtest": {
- "upload": "Upload",
- "download": "Download",
- "ping": "Ping"
+ "upload": "Pujada",
+ "download": "Baixada",
+ "ping": "Latència"
},
"portainer": {
- "running": "Running",
+ "running": "En execució",
"stopped": "Aturat",
"total": "Total"
},
@@ -357,16 +359,16 @@
"address": "Adreça",
"expires": "Caduca",
"never": "Mai",
- "user": "User",
- "hostname": "Hostname",
- "name": "Name",
- "client_version": "Client Version",
+ "user": "Usuari",
+ "hostname": "Nom d'hoste",
+ "name": "Nom",
+ "client_version": "Versió de client",
"os": "OS",
- "created": "Created",
- "authorized": "Authorized",
- "is_external": "Is External",
- "update_available": "Update Available",
- "tags": "Tags",
+ "created": "Creat",
+ "authorized": "Autoritzat",
+ "is_external": "És extern",
+ "update_available": "Actualització disponible",
+ "tags": "Etiquetes",
"last_seen": "Vist per darrer cop",
"now": "Ara",
"years": "{{number}}a",
@@ -376,7 +378,7 @@
"minutes": "{{number}}m",
"seconds": "{{number}}s",
"ago": "Fa {{value}}",
- "true": "Yes",
+ "true": "Sí",
"false": "No"
},
"technitium": {
@@ -472,7 +474,7 @@
"users": "Usuaris",
"loginsLast24H": "Inicis de sessió (24h)",
"failedLoginsLast24H": "Errors d'inici de sessió (24h)",
- "authorizationsLast24H": "Auths (24h)"
+ "authorizationsLast24H": "Autentificacions (últ. 24h)"
},
"proxmox": {
"mem": "MEM",
@@ -540,37 +542,37 @@
"61-day": "Pluja lleugera",
"61-night": "Pluja lleugera",
"63-day": "Pluja",
- "63-night": "Rain",
+ "63-night": "Pluja",
"65-day": "Pluja intensa",
- "65-night": "Heavy Rain",
+ "65-night": "Pluja forta",
"66-day": "Pluja gelada",
- "66-night": "Freezing Rain",
- "67-day": "Freezing Rain",
- "67-night": "Freezing Rain",
+ "66-night": "Pluja gelada",
+ "67-day": "Pluja gelada",
+ "67-night": "Pluja gelada",
"71-day": "Neu lleugera",
- "71-night": "Light Snow",
+ "71-night": "Nevada lleugera",
"73-day": "Neu",
- "73-night": "Snow",
+ "73-night": "Nevada",
"75-day": "Neu intensa",
- "75-night": "Heavy Snow",
+ "75-night": "Nevada forta",
"77-day": "Neu lleugera",
- "77-night": "Snow Grains",
+ "77-night": "Flocs de neu",
"80-day": "Plovisqueig",
- "80-night": "Light Showers",
+ "80-night": "Plugims suaus",
"81-day": "Xàfecs",
- "81-night": "Showers",
+ "81-night": "Plugims",
"82-day": "Xàfecs intensos",
- "82-night": "Heavy Showers",
+ "82-night": "Plugims forts",
"85-day": "Xàfecs de neu",
- "85-night": "Snow Showers",
- "86-day": "Snow Showers",
- "86-night": "Snow Showers",
+ "85-night": "Plugims amb neu",
+ "86-day": "Plugims amb neu",
+ "86-night": "Plugims amb neu",
"95-day": "Tempesta",
- "95-night": "Thunderstorm",
+ "95-night": "Tormenta",
"96-day": "Tempesta amb calamarsa",
- "96-night": "Thunderstorm With Hail",
- "99-day": "Thunderstorm With Hail",
- "99-night": "Thunderstorm With Hail"
+ "96-night": "Tormenta amb calamarsa",
+ "99-day": "Tormenta amb calamarsa",
+ "99-night": "Tormenta amb calamarsa"
},
"homebridge": {
"available_update": "Sistema",
@@ -579,18 +581,18 @@
"up_to_date": "Actualitzat",
"child_bridges": "Ponts fills",
"child_bridges_status": "{{ok}}/{{total}}",
- "up": "Up",
- "pending": "Pending",
- "down": "Down",
+ "up": "En línia",
+ "pending": "Pendent",
+ "down": "Inactiu",
"ok": "Ok"
},
"healthchecks": {
"new": "Nou",
- "up": "Up",
+ "up": "Actiu",
"grace": "En Període de gràcia",
- "down": "Down",
+ "down": "Inactiu",
"paused": "En pausa",
- "status": "Status",
+ "status": "Estat",
"last_ping": "Últim ping",
"never": "Sense pings"
},
@@ -600,74 +602,74 @@
"containers_failed": "Error"
},
"autobrr": {
- "approvedPushes": "Approved",
+ "approvedPushes": "Aprovat",
"rejectedPushes": "Rebutjat",
"filters": "Filtres",
- "indexers": "Indexers"
+ "indexers": "Indexadors"
},
"tubearchivist": {
- "downloads": "Queue",
+ "downloads": "Cua",
"videos": "Vídeos",
"channels": "Canals",
"playlists": "Llistes de reproducció"
},
"truenas": {
"load": "Càrrega del sistema",
- "uptime": "Uptime",
- "alerts": "Alerts"
+ "uptime": "Temps actiu",
+ "alerts": "Alertes"
},
"pyload": {
"speed": "Velocitat",
- "active": "Active",
- "queue": "Queue",
+ "active": "Actiu",
+ "queue": "Cua",
"total": "Total"
},
"gluetun": {
"public_ip": "IP Pública",
"region": "Regió",
"country": "País",
- "port_forwarded": "Port Forwarded"
+ "port_forwarded": "Ports redirigits"
},
"hdhomerun": {
- "channels": "Channels",
+ "channels": "Canals",
"hd": "HD",
"tunerCount": "Sintonitzadors",
"channelNumber": "Canal",
"channelNetwork": "Xarxa",
"signalStrength": "Intensitat",
"signalQuality": "Qualitat",
- "symbolQuality": "Quality",
- "networkRate": "Bitrate",
+ "symbolQuality": "Qualitat",
+ "networkRate": "Taxa de bits",
"clientIP": "Client"
},
"scrutiny": {
"passed": "Aprovat",
- "failed": "Failed",
- "unknown": "Unknown"
+ "failed": "Error",
+ "unknown": "Desconegut"
},
"paperlessngx": {
"inbox": "Safata d'entrada",
"total": "Total"
},
"pangolin": {
- "orgs": "Orgs",
- "sites": "Sites",
- "resources": "Resources",
- "targets": "Targets",
- "traffic": "Traffic",
- "in": "In",
- "out": "Out"
+ "orgs": "Organitzacions",
+ "sites": "Llocs",
+ "resources": "Recursos",
+ "targets": "Objectius",
+ "traffic": "Tràfic",
+ "in": "Dins",
+ "out": "Fora"
},
"peanut": {
"battery_charge": "Càrrega de la bateria",
"ups_load": "Càrrega del SAI",
"ups_status": "Estat del SAI",
- "online": "Online",
+ "online": "En línia",
"on_battery": "En Bateria",
"low_battery": "Bateria Baixa"
},
"nextdns": {
- "wait": "Please Wait",
+ "wait": "Espereu si us plau",
"no_devices": "No s'han rebut dades del Dispositiu"
},
"mikrotik": {
@@ -855,37 +857,37 @@
"jdownloader": {
"downloadCount": "Cua",
"downloadBytesRemaining": "Restant",
- "downloadTotalBytes": "Size",
- "downloadSpeed": "Speed"
+ "downloadTotalBytes": "Mida",
+ "downloadSpeed": "Velocitat"
},
"kavita": {
- "seriesCount": "Series",
- "totalFiles": "Files"
+ "seriesCount": "Sèries",
+ "totalFiles": "Fitxers"
},
"azuredevops": {
"result": "Resultat",
- "status": "Status",
+ "status": "Estat",
"buildId": "Id de compilació",
"succeeded": "Amb èxit",
"notStarted": "No Iniciat",
- "failed": "Failed",
+ "failed": "Error",
"canceled": "Cancel·lat",
"inProgress": "En curs",
"totalPrs": "PRs Totals",
"myPrs": "Les meves PRs",
- "approved": "Approved"
+ "approved": "Aprovat"
},
"gamedig": {
- "status": "Status",
- "online": "Online",
- "offline": "Offline",
+ "status": "Estat",
+ "online": "En línia",
+ "offline": "Desconnectat",
"name": "Nom",
"map": "Mapa",
"currentPlayers": "Jugadors actuals",
- "players": "Players",
+ "players": "Jugadors",
"maxPlayers": "Màxim de jugadors",
"bots": "Bots",
- "ping": "Ping"
+ "ping": "Latència"
},
"urbackup": {
"ok": "Ok",
@@ -895,39 +897,39 @@
},
"mealie": {
"recipes": "Receptes",
- "users": "Users",
+ "users": "Usuaris",
"categories": "Categories",
"tags": "Etiquetes"
},
"openmediavault": {
"downloading": "Descarregant",
"total": "Total",
- "running": "Running",
- "stopped": "Stopped",
- "passed": "Passed",
- "failed": "Failed"
+ "running": "En execució",
+ "stopped": "Aturat",
+ "passed": "Aprovat",
+ "failed": "Error"
},
"openwrt": {
- "uptime": "Uptime",
+ "uptime": "Temps actiu",
"cpuLoad": "Càrrega mitjana de CPU (5min)",
- "up": "Up",
- "down": "Down",
+ "up": "En línia",
+ "down": "Desconnectat",
"bytesTx": "Enviat",
- "bytesRx": "Received"
+ "bytesRx": "Rebut"
},
"uptimerobot": {
- "status": "Status",
- "uptime": "Uptime",
+ "status": "Estat",
+ "uptime": "Temps actiu",
"lastDown": "Darrera Inactivitat",
"downDuration": "Duració d'Inactivitat",
- "sitesUp": "Sites Up",
- "sitesDown": "Sites Down",
- "paused": "Paused",
+ "sitesUp": "Pàgines actives",
+ "sitesDown": "Pàgines inactives",
+ "paused": "Pausat",
"notyetchecked": "Sense verificar",
- "up": "Up",
+ "up": "En línia",
"seemsdown": "Sembla caigut",
- "down": "Down",
- "unknown": "Unknown"
+ "down": "Desconnectat",
+ "unknown": "Desconegut"
},
"calendar": {
"inCinemas": "En cines",
@@ -935,7 +937,7 @@
"digitalRelease": "Estrena digital",
"noEventsToday": "Cap esdeveniment per avui!",
"noEventsFound": "No s'han trobat esdeveniments",
- "errorWhenLoadingData": "Error when loading calendar data"
+ "errorWhenLoadingData": "Error carregant dades del calendari"
},
"romm": {
"platforms": "Plataformes",
@@ -946,50 +948,50 @@
"totalfilesize": "Tamany total"
},
"mailcow": {
- "domains": "Domains",
+ "domains": "Dominis",
"mailboxes": "Bústies",
"mails": "Correus",
- "storage": "Storage"
+ "storage": "Emmagatzematge"
},
"maintainerr": {
- "itemsHandled": "Items Handled",
- "episodesHandled": "Episodes Handled",
- "moviesHandled": "Movies Handled",
- "showsHandled": "Shows Handled",
- "seasonsHandled": "Seasons Handled",
- "reclaimable": "Reclaimable",
- "movieReclaimable": "Movie Reclaimable",
- "showReclaimable": "Show Reclaimable",
- "totalCapacity": "Total Capacity"
+ "itemsHandled": "Items gestionats",
+ "episodesHandled": "Episodis gestionats",
+ "moviesHandled": "Pel·lícules gestionades",
+ "showsHandled": "Sèries gestionades",
+ "seasonsHandled": "Temporades gestionades",
+ "reclaimable": "Reclamable",
+ "movieReclaimable": "Pel·lícula reclamable",
+ "showReclaimable": "Sèrie reclamable",
+ "totalCapacity": "Capacitat total"
},
"netdata": {
"warnings": "Avisos",
"criticals": "Crítics"
},
"ntfy": {
- "title": "Title",
- "priority": "Priority",
- "lastReceived": "Last Received",
- "message": "Message",
- "tags": "Tags",
- "none": "None",
+ "title": "Títol",
+ "priority": "Prioritat",
+ "lastReceived": "Rebut per últim cop",
+ "message": "Missatge",
+ "tags": "Etiquetes",
+ "none": "Cap",
"min": "Min",
- "low": "Low",
- "default": "Default",
- "high": "High",
+ "low": "Baix",
+ "default": "Per defecte",
+ "high": "Alt",
"urgent": "Urgent"
},
"plantit": {
"events": "Esdeveniments",
"plants": "Plantes",
- "photos": "Photos",
+ "photos": "Fotos",
"species": "Espècies"
},
"gitea": {
"notifications": "Notificacions",
- "issues": "Issues",
+ "issues": "Problemes",
"pulls": "Sol·licitud de Canvis",
- "repositories": "Repositories"
+ "repositories": "Repositoris"
},
"stash": {
"scenes": "Escenes",
@@ -1003,13 +1005,13 @@
"galleries": "Biblioteques",
"performers": "Intèrprets",
"studios": "Estudis",
- "movies": "Movies",
- "tags": "Tags",
+ "movies": "Pel·lícules",
+ "tags": "Etiquetes",
"oCount": "O Count"
},
"tandoor": {
- "users": "Users",
- "recipes": "Recipes",
+ "users": "Usuaris",
+ "recipes": "Receptes",
"keywords": "Paraules claus"
},
"homebox": {
@@ -1017,17 +1019,17 @@
"totalWithWarranty": "Amb Garantia",
"locations": "Ubicacions",
"labels": "Etiquetes",
- "users": "Users",
+ "users": "Usuaris",
"totalValue": "Valor total"
},
"crowdsec": {
- "alerts": "Alerts",
+ "alerts": "Alertes",
"bans": "Prohibicions"
},
"wgeasy": {
- "connected": "Connected",
- "enabled": "Enabled",
- "disabled": "Disabled",
+ "connected": "Connectat",
+ "enabled": "Habilitat",
+ "disabled": "Deshabilitat",
"total": "Total"
},
"swagdashboard": {
@@ -1037,9 +1039,9 @@
"banned": "Bloquejat"
},
"myspeed": {
- "ping": "Ping",
- "download": "Download",
- "upload": "Upload"
+ "ping": "Latència",
+ "download": "Baixada",
+ "upload": "Pujada"
},
"stocks": {
"stocks": "Accions",
@@ -1136,120 +1138,120 @@
"tags": "Etiquetes"
},
"slskd": {
- "slskStatus": "Network",
- "connected": "Connected",
- "disconnected": "Disconnected",
- "updateStatus": "Update",
- "update_yes": "Available",
- "update_no": "Up to Date",
- "downloads": "Downloads",
- "uploads": "Uploads",
- "sharedFiles": "Files"
+ "slskStatus": "Xarxa",
+ "connected": "Connectat",
+ "disconnected": "Desconnectat",
+ "updateStatus": "Actualitza",
+ "update_yes": "Disponible",
+ "update_no": "Actualitzat",
+ "downloads": "Descàrregues",
+ "uploads": "Pujades",
+ "sharedFiles": "Fitxers"
},
"jellystat": {
- "songs": "Songs",
- "movies": "Movies",
- "episodes": "Episodes",
- "other": "Other"
+ "songs": "Cançons",
+ "movies": "Pel·lícules",
+ "episodes": "Episodis",
+ "other": "Altres"
},
"checkmk": {
- "serviceErrors": "Service issues",
- "hostErrors": "Host issues"
+ "serviceErrors": "Errors de servei",
+ "hostErrors": "Problemes de l'hoste"
},
"komodo": {
"total": "Total",
- "running": "Running",
- "stopped": "Stopped",
- "down": "Down",
- "unhealthy": "Unhealthy",
- "unknown": "Unknown",
- "servers": "Servers",
+ "running": "En execució",
+ "stopped": "Aturat",
+ "down": "Fora de servei",
+ "unhealthy": "No saludable",
+ "unknown": "Desconegut",
+ "servers": "Servidors",
"stacks": "Stacks",
- "containers": "Containers"
+ "containers": "Contenidors"
},
"filebrowser": {
- "available": "Available",
- "used": "Used",
+ "available": "Disponible",
+ "used": "Utilitzat",
"total": "Total"
},
"wallos": {
- "activeSubscriptions": "Subscriptions",
- "thisMonthlyCost": "This Month",
- "nextMonthlyCost": "Next Month",
- "previousMonthlyCost": "Prev. Month",
- "nextRenewingSubscription": "Next Payment"
+ "activeSubscriptions": "Subscripcions",
+ "thisMonthlyCost": "Aquest mes",
+ "nextMonthlyCost": "Mes que ve",
+ "previousMonthlyCost": "Mes passat",
+ "nextRenewingSubscription": "Proper pagament"
},
"unraid": {
- "STARTED": "Started",
- "STOPPED": "Stopped",
- "NEW_ARRAY": "New Array",
- "RECON_DISK": "Reconstructing Disk",
- "DISABLE_DISK": "Disk Disabled",
- "SWAP_DSBL": "Swap Disable",
- "INVALID_EXPANSION": "Invalid Expansion",
- "PARITY_NOT_BIGGEST": "Parity Not Biggest",
- "TOO_MANY_MISSING_DISKS": "Too Many Missing Disks",
- "NEW_DISK_TOO_SMALL": "New Disk Too Small",
- "NO_DATA_DISKS": "No Data Disks",
- "notifications": "Notifications",
- "status": "Status",
+ "STARTED": "Iniciat",
+ "STOPPED": "Aturat",
+ "NEW_ARRAY": "Nou array",
+ "RECON_DISK": "Reconstruint disc",
+ "DISABLE_DISK": "Disc desactivat",
+ "SWAP_DSBL": "Swap desactivat",
+ "INVALID_EXPANSION": "Expansió invàlida",
+ "PARITY_NOT_BIGGEST": "Paritat no del més gran",
+ "TOO_MANY_MISSING_DISKS": "Falten masses discs",
+ "NEW_DISK_TOO_SMALL": "Nou disc massa petit",
+ "NO_DATA_DISKS": "Sense discs de dades",
+ "notifications": "Notificacions",
+ "status": "Estat",
"cpu": "CPU",
- "memoryUsed": "Memory Used",
- "memoryAvailable": "Memory Available",
- "arrayUsed": "Array Used",
- "arrayFree": "Array Free",
- "poolUsed": "{{pool}} Used",
- "poolFree": "{{pool}} Free"
+ "memoryUsed": "Memòria emprada",
+ "memoryAvailable": "Memòria disponible",
+ "arrayUsed": "Array en ús",
+ "arrayFree": "Array lliure",
+ "poolUsed": "{{pool}} en ús",
+ "poolFree": "{{pool}} lliure"
},
"backrest": {
"num_plans": "Plans",
- "num_success_30": "Successes",
- "num_failure_30": "Failures",
- "num_success_latest": "Succeeding",
- "num_failure_latest": "Failing",
- "bytes_added_30": "Bytes Added"
+ "num_success_30": "Èxits",
+ "num_failure_30": "Fallades",
+ "num_success_latest": "Triomfant",
+ "num_failure_latest": "Fallant",
+ "bytes_added_30": "Bytes afegits"
},
"yourspotify": {
- "songs": "Songs",
- "time": "Time",
- "artists": "Artists"
+ "songs": "Cançons",
+ "time": "Temps",
+ "artists": "Artistes"
},
"arcane": {
- "containers": "Containers",
- "images": "Images",
- "image_updates": "Image Updates",
- "images_unused": "Unused",
- "environment_required": "Environment ID Required"
+ "containers": "Contenidors",
+ "images": "Imatges",
+ "image_updates": "Actualitzacions d'imatges",
+ "images_unused": "Sense utilitzar",
+ "environment_required": "ID d'entorn requerit"
},
"dockhand": {
- "running": "Running",
- "stopped": "Stopped",
+ "running": "En execució",
+ "stopped": "Aturats",
"cpu": "CPU",
- "memory": "Memory",
- "images": "Images",
- "volumes": "Volumes",
- "events_today": "Events Today",
- "pending_updates": "Pending Updates",
+ "memory": "Memòria",
+ "images": "Imatges",
+ "volumes": "Volums",
+ "events_today": "Esdeveniments avui",
+ "pending_updates": "Actualitzacions pendents",
"stacks": "Stacks",
- "paused": "Paused",
+ "paused": "Pausats",
"total": "Total",
- "environment_not_found": "Environment Not Found"
+ "environment_not_found": "Entorn no trobat"
},
"sparkyfitness": {
- "eaten": "Eaten",
- "burned": "Burned",
- "remaining": "Remaining",
- "steps": "Steps"
+ "eaten": "Menjat",
+ "burned": "Cremat",
+ "remaining": "Restant",
+ "steps": "Passes"
},
"syncthing": {
- "connected": "Connected",
- "synced": "Synced",
+ "connected": "Connectat",
+ "synced": "Sincronitzat",
"errors": "Errors",
- "storage": "Storage"
+ "storage": "Emmagatzematge"
},
"sportarr": {
- "wanted": "Wanted",
- "queued": "Queued",
- "leagues": "Leagues"
+ "wanted": "Volgut",
+ "queued": "Encuat",
+ "leagues": "Lligues"
}
}
diff --git a/public/locales/cs/common.json b/public/locales/cs/common.json
index 275a6f202..021ec116b 100644
--- a/public/locales/cs/common.json
+++ b/public/locales/cs/common.json
@@ -39,7 +39,9 @@
"wait": "Počkejte prosím"
},
"search": {
- "placeholder": "Hledat…"
+ "placeholder": "Hledat…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "Využití procesoru",
diff --git a/public/locales/da/common.json b/public/locales/da/common.json
index da4f22817..dfbe71c26 100644
--- a/public/locales/da/common.json
+++ b/public/locales/da/common.json
@@ -39,7 +39,9 @@
"wait": "Vent venligst"
},
"search": {
- "placeholder": "Søg…"
+ "placeholder": "Søg…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/de/common.json b/public/locales/de/common.json
index 97ac2c198..48adf183d 100644
--- a/public/locales/de/common.json
+++ b/public/locales/de/common.json
@@ -39,7 +39,9 @@
"wait": "Bitte warten"
},
"search": {
- "placeholder": "Suche…"
+ "placeholder": "Suche…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/el/common.json b/public/locales/el/common.json
index 9af34a384..4b2c74bf6 100644
--- a/public/locales/el/common.json
+++ b/public/locales/el/common.json
@@ -39,7 +39,9 @@
"wait": "Παρακαλώ περιμένετε"
},
"search": {
- "placeholder": "Αναζήτηση…"
+ "placeholder": "Αναζήτηση…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "Επεξεργαστής",
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index cb72b9f4d..e355c2efd 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -39,7 +39,9 @@
"wait": "Please wait"
},
"search": {
- "placeholder": "Search…"
+ "placeholder": "Search…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/eo/common.json b/public/locales/eo/common.json
index b534f49c3..626a0711a 100644
--- a/public/locales/eo/common.json
+++ b/public/locales/eo/common.json
@@ -39,7 +39,9 @@
"wait": "Bonvolu atendi"
},
"search": {
- "placeholder": "Serĉi…"
+ "placeholder": "Serĉi…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "Ĉefprocesoro",
diff --git a/public/locales/es/common.json b/public/locales/es/common.json
index 91c68b309..ded79c26a 100644
--- a/public/locales/es/common.json
+++ b/public/locales/es/common.json
@@ -39,7 +39,9 @@
"wait": "Espera, por favor"
},
"search": {
- "placeholder": "Buscar…"
+ "placeholder": "Buscar…",
+ "provider": "Motor de búsqueda: {{name}}",
+ "select_provider": "Seleccionar motores de búsqueda"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/eu/common.json b/public/locales/eu/common.json
index 625b0584a..a56d395e4 100644
--- a/public/locales/eu/common.json
+++ b/public/locales/eu/common.json
@@ -39,7 +39,9 @@
"wait": "Itxaron mesedez"
},
"search": {
- "placeholder": "Bilatu…"
+ "placeholder": "Bilatu…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/fi/common.json b/public/locales/fi/common.json
index c11dc8968..a6fa57228 100644
--- a/public/locales/fi/common.json
+++ b/public/locales/fi/common.json
@@ -39,7 +39,9 @@
"wait": "Odota, ole hyvä"
},
"search": {
- "placeholder": "Hae…"
+ "placeholder": "Hae…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/fr/common.json b/public/locales/fr/common.json
index 27a770bd6..697524468 100644
--- a/public/locales/fr/common.json
+++ b/public/locales/fr/common.json
@@ -39,7 +39,9 @@
"wait": "Veuillez patienter"
},
"search": {
- "placeholder": "Recherche…"
+ "placeholder": "Recherche…",
+ "provider": "Moteur de recherche : {{name}}",
+ "select_provider": "Sélectionnez un moteur de recherche"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/he/common.json b/public/locales/he/common.json
index 5b3cff9c5..b0928d031 100644
--- a/public/locales/he/common.json
+++ b/public/locales/he/common.json
@@ -39,7 +39,9 @@
"wait": "המתן בבקשה"
},
"search": {
- "placeholder": "חיפוש…"
+ "placeholder": "חיפוש…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "מעבד",
diff --git a/public/locales/hi/common.json b/public/locales/hi/common.json
index 3d20dd74a..f114a6a76 100644
--- a/public/locales/hi/common.json
+++ b/public/locales/hi/common.json
@@ -39,7 +39,9 @@
"wait": "Please wait"
},
"search": {
- "placeholder": "Search…"
+ "placeholder": "Search…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/hr/common.json b/public/locales/hr/common.json
index 67bef0514..c849326c3 100644
--- a/public/locales/hr/common.json
+++ b/public/locales/hr/common.json
@@ -21,7 +21,7 @@
"seconds": "s"
},
"auth": {
- "signout": "Sign out"
+ "signout": "Odjavi se"
},
"widget": {
"missing_type": "Nedostajuća vrsta widgeta: {{type}}",
@@ -39,7 +39,9 @@
"wait": "Pričekaj"
},
"search": {
- "placeholder": "Traži …"
+ "placeholder": "Traži …",
+ "provider": "Tražilica: {{name}}",
+ "select_provider": "Odaberi tražilicu"
},
"resources": {
"cpu": "CPU",
@@ -133,7 +135,7 @@
"series": "Serije",
"episodes": "Epizode",
"songs": "Pjesme",
- "albums": "Albums"
+ "albums": "Albumi"
},
"esphome": {
"offline": "Offline",
@@ -472,7 +474,7 @@
"users": "Korisnici",
"loginsLast24H": "Prijave (24 h)",
"failedLoginsLast24H": "Neuspjele prijave (24 h)",
- "authorizationsLast24H": "Auths (24h)"
+ "authorizationsLast24H": "Autorizacije (24h)"
},
"proxmox": {
"mem": "MEM",
@@ -952,15 +954,15 @@
"storage": "Spremište"
},
"maintainerr": {
- "itemsHandled": "Items Handled",
- "episodesHandled": "Episodes Handled",
- "moviesHandled": "Movies Handled",
- "showsHandled": "Shows Handled",
- "seasonsHandled": "Seasons Handled",
+ "itemsHandled": "Obrađene stavke",
+ "episodesHandled": "Obrađene epizode",
+ "moviesHandled": "Obrađeni filmovi",
+ "showsHandled": "Obrađene emisije",
+ "seasonsHandled": "Obrađene sezone",
"reclaimable": "Reclaimable",
"movieReclaimable": "Movie Reclaimable",
"showReclaimable": "Show Reclaimable",
- "totalCapacity": "Total Capacity"
+ "totalCapacity": "Ukupni kapacitet"
},
"netdata": {
"warnings": "Upozorenja",
@@ -1248,8 +1250,8 @@
"storage": "Spremište"
},
"sportarr": {
- "wanted": "Wanted",
- "queued": "Queued",
- "leagues": "Leagues"
+ "wanted": "Željeno",
+ "queued": "U redu čekanja",
+ "leagues": "Lige"
}
}
diff --git a/public/locales/hu/common.json b/public/locales/hu/common.json
index f573aab0b..979474218 100644
--- a/public/locales/hu/common.json
+++ b/public/locales/hu/common.json
@@ -39,7 +39,9 @@
"wait": "Kérjük várjon"
},
"search": {
- "placeholder": "Keresés…"
+ "placeholder": "Keresés…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "Processzor",
diff --git a/public/locales/id/common.json b/public/locales/id/common.json
index 088ebfbe6..96bd6f036 100644
--- a/public/locales/id/common.json
+++ b/public/locales/id/common.json
@@ -39,7 +39,9 @@
"wait": "Harap tunggu"
},
"search": {
- "placeholder": "Telusuri…"
+ "placeholder": "Telusuri…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/it/common.json b/public/locales/it/common.json
index 583c8014a..278107b7d 100644
--- a/public/locales/it/common.json
+++ b/public/locales/it/common.json
@@ -39,7 +39,9 @@
"wait": "Attendi per favore"
},
"search": {
- "placeholder": "Cerca…"
+ "placeholder": "Cerca…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json
index b776fc63d..2efcb8b6d 100644
--- a/public/locales/ja/common.json
+++ b/public/locales/ja/common.json
@@ -39,7 +39,9 @@
"wait": "お待ちください"
},
"search": {
- "placeholder": "検索…"
+ "placeholder": "検索…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/ko/common.json b/public/locales/ko/common.json
index 230e5d3f9..7b6e0a9be 100644
--- a/public/locales/ko/common.json
+++ b/public/locales/ko/common.json
@@ -39,7 +39,9 @@
"wait": "잠시만 기다리세요"
},
"search": {
- "placeholder": "검색…"
+ "placeholder": "검색…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
@@ -472,7 +474,7 @@
"users": "사용자",
"loginsLast24H": "로그인 (24시간)",
"failedLoginsLast24H": "실패한 로그인 (24시간)",
- "authorizationsLast24H": "Auths (24h)"
+ "authorizationsLast24H": "인증(24시간)"
},
"proxmox": {
"mem": "메모리",
diff --git a/public/locales/lv/common.json b/public/locales/lv/common.json
index 3f2212ecb..7e3da5e2c 100644
--- a/public/locales/lv/common.json
+++ b/public/locales/lv/common.json
@@ -39,7 +39,9 @@
"wait": "Lūdzu, uzgaidiet"
},
"search": {
- "placeholder": "Meklēt…"
+ "placeholder": "Meklēt…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/ms/common.json b/public/locales/ms/common.json
index 9c47efbf8..58aedd33c 100644
--- a/public/locales/ms/common.json
+++ b/public/locales/ms/common.json
@@ -39,7 +39,9 @@
"wait": "Sila tunggu"
},
"search": {
- "placeholder": "Carian…"
+ "placeholder": "Carian…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/nl/common.json b/public/locales/nl/common.json
index 3bec3fb94..4a30e547d 100644
--- a/public/locales/nl/common.json
+++ b/public/locales/nl/common.json
@@ -39,7 +39,9 @@
"wait": "Even geduld"
},
"search": {
- "placeholder": "Zoeken…"
+ "placeholder": "Zoeken…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/no/common.json b/public/locales/no/common.json
index df2e839cb..f94efeb7d 100644
--- a/public/locales/no/common.json
+++ b/public/locales/no/common.json
@@ -39,7 +39,9 @@
"wait": "Vennligst vent"
},
"search": {
- "placeholder": "Søk…"
+ "placeholder": "Søk…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/pl/common.json b/public/locales/pl/common.json
index 96babaef4..f00d21d25 100644
--- a/public/locales/pl/common.json
+++ b/public/locales/pl/common.json
@@ -39,7 +39,9 @@
"wait": "Proszę czekać"
},
"search": {
- "placeholder": "Szukaj…"
+ "placeholder": "Szukaj…",
+ "provider": "Wyszukiwarka: {{name}}",
+ "select_provider": "Wybierz wyszukiwarkę"
},
"resources": {
"cpu": "Procesor",
@@ -472,7 +474,7 @@
"users": "Użytkownicy",
"loginsLast24H": "Logowania (24h)",
"failedLoginsLast24H": "Nieudane logowania (24h)",
- "authorizationsLast24H": "Auths (24h)"
+ "authorizationsLast24H": "Uwierzytelnienia (24h)"
},
"proxmox": {
"mem": "RAM",
diff --git a/public/locales/pt/common.json b/public/locales/pt/common.json
index 7d1393659..bf5687cfb 100644
--- a/public/locales/pt/common.json
+++ b/public/locales/pt/common.json
@@ -39,7 +39,9 @@
"wait": "Por favor aguarde"
},
"search": {
- "placeholder": "Pesquisar…"
+ "placeholder": "Pesquisar…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/pt_BR/common.json b/public/locales/pt_BR/common.json
index 2f9cc91cb..09afc6d12 100644
--- a/public/locales/pt_BR/common.json
+++ b/public/locales/pt_BR/common.json
@@ -39,7 +39,9 @@
"wait": "Por favor, aguarde"
},
"search": {
- "placeholder": "Pesquisar…"
+ "placeholder": "Pesquisar…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/ro/common.json b/public/locales/ro/common.json
index 828664534..75293f677 100644
--- a/public/locales/ro/common.json
+++ b/public/locales/ro/common.json
@@ -39,7 +39,9 @@
"wait": "Va rugăm așteptați"
},
"search": {
- "placeholder": "Caută…"
+ "placeholder": "Caută…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "Procesor",
diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json
index 4b8dc07d7..90c8193e2 100644
--- a/public/locales/ru/common.json
+++ b/public/locales/ru/common.json
@@ -39,7 +39,9 @@
"wait": "Пожалуйста, подождите"
},
"search": {
- "placeholder": "Поиск…"
+ "placeholder": "Поиск…",
+ "provider": "Поисковая система: {{name}}",
+ "select_provider": "Выберите поисковую систему"
},
"resources": {
"cpu": "ЦП",
diff --git a/public/locales/sk/common.json b/public/locales/sk/common.json
index eb181f410..421335bed 100644
--- a/public/locales/sk/common.json
+++ b/public/locales/sk/common.json
@@ -39,7 +39,9 @@
"wait": "Počkajte prosím"
},
"search": {
- "placeholder": "Hľadať…"
+ "placeholder": "Hľadať…",
+ "provider": "Vyhľadávač: {{name}}",
+ "select_provider": "Vyberte vyhľadávač"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/sl/common.json b/public/locales/sl/common.json
index 644d22f9f..086d41f5e 100644
--- a/public/locales/sl/common.json
+++ b/public/locales/sl/common.json
@@ -39,7 +39,9 @@
"wait": "Prosimo počakajte"
},
"search": {
- "placeholder": "Iskanje…"
+ "placeholder": "Iskanje…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/sr/common.json b/public/locales/sr/common.json
index 1d852c3e8..19c90547c 100644
--- a/public/locales/sr/common.json
+++ b/public/locales/sr/common.json
@@ -39,7 +39,9 @@
"wait": "Молим сачекајте"
},
"search": {
- "placeholder": "Претражи…"
+ "placeholder": "Претражи…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "Процесор",
diff --git a/public/locales/sv/common.json b/public/locales/sv/common.json
index 8361d966f..be57d399e 100644
--- a/public/locales/sv/common.json
+++ b/public/locales/sv/common.json
@@ -39,7 +39,9 @@
"wait": "Vänligen vänta"
},
"search": {
- "placeholder": "Sök…"
+ "placeholder": "Sök…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/te/common.json b/public/locales/te/common.json
index ad7eff90e..6312f27ba 100644
--- a/public/locales/te/common.json
+++ b/public/locales/te/common.json
@@ -39,7 +39,9 @@
"wait": "దయచేసి వేచి ఉండండి"
},
"search": {
- "placeholder": "వెతకండి…"
+ "placeholder": "వెతకండి…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "సీపియూ",
diff --git a/public/locales/th/common.json b/public/locales/th/common.json
index 95176de3d..27d94d0bf 100644
--- a/public/locales/th/common.json
+++ b/public/locales/th/common.json
@@ -39,7 +39,9 @@
"wait": "โปรดรอ"
},
"search": {
- "placeholder": "ค้นหา…"
+ "placeholder": "ค้นหา…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "ซีพียู",
diff --git a/public/locales/tr/common.json b/public/locales/tr/common.json
index 5b4301c59..ed1cf64a4 100644
--- a/public/locales/tr/common.json
+++ b/public/locales/tr/common.json
@@ -39,7 +39,9 @@
"wait": "Lütfen bekleyin"
},
"search": {
- "placeholder": "Ara…"
+ "placeholder": "Ara…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "İşlemci",
diff --git a/public/locales/uk/common.json b/public/locales/uk/common.json
index 1f613ab8f..1a088b49e 100644
--- a/public/locales/uk/common.json
+++ b/public/locales/uk/common.json
@@ -39,7 +39,9 @@
"wait": "Будь ласка, зачекайте"
},
"search": {
- "placeholder": "Пошук…"
+ "placeholder": "Пошук…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "ЦП",
diff --git a/public/locales/vi/common.json b/public/locales/vi/common.json
index 6a12f3e6d..08be98b40 100644
--- a/public/locales/vi/common.json
+++ b/public/locales/vi/common.json
@@ -21,7 +21,7 @@
"seconds": "giây"
},
"auth": {
- "signout": "Sign out"
+ "signout": "Đăng xuất"
},
"widget": {
"missing_type": "Thiếu loại Widget: {{type}}",
@@ -39,7 +39,9 @@
"wait": "Vui lòng chờ"
},
"search": {
- "placeholder": "Tìm kiếm…"
+ "placeholder": "Tìm kiếm…",
+ "provider": "Công cụ tìm kiếm: {{name}}",
+ "select_provider": "Chọn công cụ tìm kiếm"
},
"resources": {
"cpu": "CPU",
@@ -48,7 +50,7 @@
"free": "Dư",
"used": "Đã dùng",
"load": "\n",
- "temp": "TEMP",
+ "temp": "Tạm thời",
"max": "Tối đa",
"uptime": "UP"
},
@@ -64,24 +66,24 @@
"wlan_devices": "Thiết bị trong mạng WLAN",
"lan_users": "Người dùng mạng LAN",
"wlan_users": "Người dùng mạng WLAN",
- "up": "UP",
- "down": "DOWN",
+ "up": "Hoạt động",
+ "down": "Không hoạt động",
"wait": "Vui lòng chờ",
"empty_data": "Trạng thái hệ thống phụ không xác định"
},
"unifi_drive": {
- "healthy": "Healthy",
- "degraded": "Degraded",
- "no_data": "No storage data available"
+ "healthy": "Bình thường",
+ "degraded": "Suy giảm",
+ "no_data": "Không có dữ liệu"
},
"duplicati": {
- "jobs": "Jobs",
- "stored": "Stored",
- "lastBackup": "Last Run",
- "nextRun": "Next Run",
- "running": "Running",
- "warnings": "Warnings",
- "errors": "Errors"
+ "jobs": "Tác vụ",
+ "stored": "Đã lưu",
+ "lastBackup": "Lần chạy cuối",
+ "nextRun": "Lần chạy tiếp theo",
+ "running": "Đang chạy",
+ "warnings": "Cảnh báo",
+ "errors": "Lỗi"
},
"docker": {
"rx": "RX",
@@ -96,8 +98,8 @@
"starting": "Đang bắt đầu",
"unhealthy": "Bất thường",
"not_found": "Không tìm thấy",
- "exited": "Exited",
- "partial": "Partial"
+ "exited": "Đã thoát",
+ "partial": "Một phần"
},
"ping": {
"error": "Lỗi",
@@ -118,35 +120,35 @@
"playing": "Đang chơi",
"transcoding": "Chuyển định dạng",
"bitrate": "Bitrate",
- "no_active": "No Active Streams",
+ "no_active": "Không có luồng đang hoạt động",
"movies": "Phim ảnh",
"series": "Series",
- "episodes": "Episodes",
+ "episodes": "Tập",
"songs": "Bài hát"
},
"jellyfin": {
- "playing": "Playing",
- "transcoding": "Transcoding",
- "bitrate": "Bitrate",
- "no_active": "No Active Streams",
- "movies": "Movies",
+ "playing": "Đang phát",
+ "transcoding": "Đang chuyển mã",
+ "bitrate": "Tốc độ bit",
+ "no_active": "Không có luồng đang hoạt động",
+ "movies": "Phim",
"series": "Series",
- "episodes": "Episodes",
- "songs": "Songs",
- "albums": "Albums"
+ "episodes": "Tập",
+ "songs": "Bài hát",
+ "albums": "Album"
},
"esphome": {
"offline": "Offline",
"offline_alt": "Offline",
"online": "Online",
- "total": "Total",
+ "total": "Tổng cộng",
"unknown": "Không xác định"
},
"evcc": {
- "pv_power": "Production",
+ "pv_power": "Công suất phát",
"battery_soc": "Pin",
"grid_power": "Lưới",
- "home_power": "Consumption",
+ "home_power": "Công suất tiêu thụ",
"charge_power": "Bộ sạc",
"kilowatt": "kW"
},
@@ -154,7 +156,7 @@
"download": "Tải xuống",
"upload": "Tải lên",
"leech": "",
- "seed": "Seed"
+ "seed": "Seeder"
},
"freshrss": {
"subscriptions": "Đăng ký",
@@ -173,115 +175,115 @@
"maxDown": "Tải xuống tối đa",
"maxUp": "Tải lên tối đa",
"down": "Down",
- "up": "Up",
- "received": "Received",
- "sent": "Sent",
- "externalIPAddress": "Ext. IP",
- "externalIPv6Address": "Ext. IPv6",
- "externalIPv6Prefix": "Ext. IPv6-Prefix"
+ "up": "Tải lên",
+ "received": "Đã nhận",
+ "sent": "Đã gửi",
+ "externalIPAddress": "Địa chỉ IP bên ngoài",
+ "externalIPv6Address": "Địa chỉ IPv6 bên ngoài",
+ "externalIPv6Prefix": "Tiền tố IPv6 bên ngoài"
},
"caddy": {
- "upstreams": "Upstreams",
- "requests": "Current requests",
- "requests_failed": "Failed requests"
+ "upstreams": "Máy chủ backend",
+ "requests": "Yêu cầu hiện tại",
+ "requests_failed": "Yêu cầu thất bại"
},
"changedetectionio": {
- "totalObserved": "Total Observed",
- "diffsDetected": "Diffs Detected"
+ "totalObserved": "Tổng số đã theo dõi",
+ "diffsDetected": "Số thay đổi đã phát hiện"
},
"channelsdvrserver": {
- "shows": "Shows",
- "recordings": "Recordings",
- "scheduled": "Scheduled",
- "passes": "Passes"
+ "shows": "Chương trình",
+ "recordings": "Bản ghi",
+ "scheduled": "Đã lên lịch",
+ "passes": "Quy tắc ghi"
},
"tautulli": {
- "playing": "Playing",
- "transcoding": "Transcoding",
- "bitrate": "Bitrate",
- "no_active": "No Active Streams",
- "plex_connection_error": "Check Plex Connection"
+ "playing": "Đang phát",
+ "transcoding": "Đang chuyển mã",
+ "bitrate": "Tốc độ bit",
+ "no_active": "Không có luồng đang hoạt động",
+ "plex_connection_error": "Kiểm tra kết nối Plex"
},
"tracearr": {
- "no_active": "No Active Streams",
- "streams": "Streams",
- "transcodes": "Transcodes",
- "directplay": "Direct Play",
- "bitrate": "Bitrate"
+ "no_active": "Không có luồng đang hoạt động",
+ "streams": "Luồng",
+ "transcodes": "Luồng chuyển mã",
+ "directplay": "Phát trực tiếp",
+ "bitrate": "Tốc độ bit"
},
"omada": {
- "connectedAp": "Connected APs",
- "activeUser": "Active devices",
- "alerts": "Alerts",
- "connectedGateways": "Connected gateways",
- "connectedSwitches": "Connected switches"
+ "connectedAp": "AP đã kết nối",
+ "activeUser": "Thiết bị đang hoạt động",
+ "alerts": "Cảnh báo",
+ "connectedGateways": "Gateway đã kết nối",
+ "connectedSwitches": "Switch đã kết nối"
},
"nzbget": {
- "rate": "Rate",
- "remaining": "Remaining",
+ "rate": "Tốc độ",
+ "remaining": "Còn lại",
"downloaded": "Đã tải"
},
"plex": {
- "streams": "Active Streams",
- "albums": "Albums",
- "movies": "Movies",
- "tv": "TV Shows"
+ "streams": "Luồng đang hoạt động",
+ "albums": "Album",
+ "movies": "Phim",
+ "tv": "Chương trình TV"
},
"sabnzbd": {
- "rate": "Rate",
+ "rate": "Tốc độ",
"queue": "Hàng chờ",
"timeleft": "Thời gian còn lại"
},
"rutorrent": {
"active": "Hoạt động",
- "upload": "Upload",
- "download": "Download"
+ "upload": "Tải lên",
+ "download": "Tải xuống"
},
"transmission": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
+ "download": "Tải xuống",
+ "upload": "Tải lên",
+ "leech": "Leecher",
+ "seed": "Seeder"
},
"qbittorrent": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
+ "download": "Tải xuống",
+ "upload": "Tải lên",
+ "leech": "Leecher",
+ "seed": "Seeder"
},
"qnap": {
- "cpuUsage": "CPU Usage",
- "memUsage": "MEM Usage",
- "systemTempC": "System Temp",
- "poolUsage": "Pool Usage",
- "volumeUsage": "Volume Usage",
- "invalid": "Invalid"
+ "cpuUsage": "Mức sử dụng CPU",
+ "memUsage": "Mức sử dụng bộ nhớ",
+ "systemTempC": "Nhiệt độ hệ thống",
+ "poolUsage": "Mức sử dụng pool",
+ "volumeUsage": "Mức sử dụng volume",
+ "invalid": "Không hợp lệ"
},
"deluge": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
+ "download": "Tải xuống",
+ "upload": "Tải lên",
+ "leech": "Leecher",
+ "seed": "Seeder"
},
"develancacheui": {
- "cachehitbytes": "Cache Hit Bytes",
- "cachemissbytes": "Cache Miss Bytes"
+ "cachehitbytes": "Dung lượng cache hit",
+ "cachemissbytes": "Dung lượng cache miss"
},
"downloadstation": {
- "download": "Download",
- "upload": "Upload",
- "leech": "Leech",
- "seed": "Seed"
+ "download": "Tải xuống",
+ "upload": "Tải lên",
+ "leech": "Leecher",
+ "seed": "Seeder"
},
"sonarr": {
- "wanted": "Wanted",
+ "wanted": "Cần tải",
"queued": "Queued",
- "series": "Series",
+ "series": "Phim bộ",
"queue": "Queue",
"unknown": "Unknown"
},
"radarr": {
- "wanted": "Wanted",
+ "wanted": "Cần tải",
"missing": "Missing",
"queued": "Queued",
"movies": "Movies",
@@ -289,12 +291,12 @@
"unknown": "Unknown"
},
"lidarr": {
- "wanted": "Wanted",
+ "wanted": "Cần tải",
"queued": "Queued",
"artists": "Artists"
},
"readarr": {
- "wanted": "Wanted",
+ "wanted": "Cần tải",
"queued": "Queued",
"books": "Sách"
},
@@ -334,66 +336,66 @@
"latency": "Latency"
},
"speedtest": {
- "upload": "Upload",
- "download": "Download",
+ "upload": "Tải lên",
+ "download": "Tải xuống",
"ping": "Ping"
},
"portainer": {
- "running": "Running",
- "stopped": "Stopped",
- "total": "Total"
+ "running": "Đang chạy",
+ "stopped": "Đã dừng",
+ "total": "Tổng cộng"
},
"suwayomi": {
- "download": "Downloaded",
- "nondownload": "Non-Downloaded",
- "read": "Read",
- "unread": "Unread",
- "downloadedread": "Downloaded & Read",
- "downloadedunread": "Downloaded & Unread",
- "nondownloadedread": "Non-Downloaded & Read",
- "nondownloadedunread": "Non-Downloaded & Unread"
+ "download": "Đã tải xuống",
+ "nondownload": "Chưa tải xuống",
+ "read": "Đã đọc",
+ "unread": "Chưa đọc",
+ "downloadedread": "Đã tải xuống & Đã đọc",
+ "downloadedunread": "Đã tải xuống & Chưa đọc",
+ "nondownloadedread": "Chưa tải xuống & Đã đọc",
+ "nondownloadedunread": "Chưa tải xuống & Chưa đọc"
},
"tailscale": {
- "address": "Address",
- "expires": "Expires",
- "never": "Never",
- "user": "User",
- "hostname": "Hostname",
- "name": "Name",
- "client_version": "Client Version",
- "os": "OS",
- "created": "Created",
- "authorized": "Authorized",
- "is_external": "Is External",
- "update_available": "Update Available",
- "tags": "Tags",
- "last_seen": "Last Seen",
- "now": "Now",
- "years": "{{number}}y",
- "weeks": "{{number}}w",
- "days": "{{number}}d",
- "hours": "{{number}}h",
- "minutes": "{{number}}m",
- "seconds": "{{number}}s",
- "ago": "{{value}} Ago",
- "true": "Yes",
- "false": "No"
+ "address": "Địa chỉ",
+ "expires": "Hết hạn",
+ "never": "Không bao giờ",
+ "user": "Người dùng",
+ "hostname": "Tên máy chủ",
+ "name": "Tên",
+ "client_version": "Phiên bản máy khách",
+ "os": "Hệ điều hành",
+ "created": "Đã tạo",
+ "authorized": "Đã cấp quyền",
+ "is_external": "Là bên ngoài",
+ "update_available": "Có bản cập nhật",
+ "tags": "Thẻ",
+ "last_seen": "Lần cuối nhìn thấy",
+ "now": "Bây giờ",
+ "years": "{{number}} năm",
+ "weeks": "{{number}} tuần",
+ "days": "{{number}} ngày",
+ "hours": "{{number}} giờ",
+ "minutes": "{{number}} phút",
+ "seconds": "{{number}} giây",
+ "ago": "{{value}} trước",
+ "true": "Có",
+ "false": "Không"
},
"technitium": {
- "totalQueries": "Queries",
- "totalNoError": "Success",
- "totalServerFailure": "Failures",
- "totalNxDomain": "NX Domains",
- "totalRefused": "Refused",
+ "totalQueries": "Tổng số truy vấn",
+ "totalNoError": "Thành công",
+ "totalServerFailure": "Lỗi máy chủ",
+ "totalNxDomain": "NX Domain",
+ "totalRefused": "Bị từ chối",
"totalAuthoritative": "Authoritative",
"totalRecursive": "Recursive",
- "totalCached": "Cached",
- "totalBlocked": "Blocked",
- "totalDropped": "Dropped",
- "totalClients": "Clients"
+ "totalCached": "Được lưu trong bộ nhớ đệm",
+ "totalBlocked": "Bị chặn",
+ "totalDropped": "Bị loại bỏ",
+ "totalClients": "Số máy khách"
},
"tdarr": {
- "queue": "Queue",
+ "queue": "Hàng đợi",
"processed": "Processed",
"errored": "Errored",
"saved": "Saved"
@@ -410,7 +412,7 @@
"unknown": "Unknown"
},
"navidrome": {
- "nothing_streaming": "No Active Streams",
+ "nothing_streaming": "Không có luồng đang hoạt động",
"please_wait": "Please Wait"
},
"npm": {
@@ -453,9 +455,9 @@
"domain_count": "Domains"
},
"medusa": {
- "wanted": "Wanted",
+ "wanted": "Cần tải",
"queued": "Queued",
- "series": "Series"
+ "series": "Phim bộ"
},
"minecraft": {
"players": "Players",
@@ -472,66 +474,66 @@
"users": "Users",
"loginsLast24H": "Logins (24h)",
"failedLoginsLast24H": "Failed Logins (24h)",
- "authorizationsLast24H": "Auths (24h)"
+ "authorizationsLast24H": "Lượt ủy quyền (24 giờ)"
},
"proxmox": {
- "mem": "MEM",
+ "mem": "Bộ nhớ",
"cpu": "CPU",
"lxc": "LXC",
- "vms": "VMs"
+ "vms": "VM"
},
"pulse": {
- "nodes": "Nodes",
- "vms": "VMs",
- "lxcs": "LXCs"
+ "nodes": "Node",
+ "vms": "VM",
+ "lxcs": "LXC"
},
"glances": {
"cpu": "CPU",
- "load": "Load",
- "wait": "Please wait",
- "temp": "TEMP",
- "_temp": "Temp",
- "warn": "Warn",
- "uptime": "UP",
- "total": "Total",
- "free": "Free",
- "used": "Used",
- "days": "d",
- "hours": "h",
- "crit": "Crit",
- "read": "Read",
- "write": "Write",
+ "load": "Tải hệ thống",
+ "wait": "Vui lòng đợi",
+ "temp": "Nhiệt độ",
+ "_temp": "Nhiệt độ",
+ "warn": "Cảnh báo",
+ "uptime": "Thời gian hoạt động",
+ "total": "Tổng cộng",
+ "free": "Còn trống",
+ "used": "Đã sử dụng",
+ "days": "ngày",
+ "hours": "giờ",
+ "crit": "Nghiêm trọng",
+ "read": "Đọc",
+ "write": "Ghi",
"gpu": "GPU",
- "mem": "Mem",
+ "mem": "Bộ nhớ",
"swap": "Swap"
},
"quicklaunch": {
- "bookmark": "Bookmark",
- "service": "Service",
- "search": "Search",
- "custom": "Custom",
- "visit": "Visit",
+ "bookmark": "Dấu trang",
+ "service": "Dịch vụ",
+ "search": "Tìm kiếm",
+ "custom": "Tùy chỉnh",
+ "visit": "Truy cập",
"url": "URL",
- "searchsuggestion": "Suggestion"
+ "searchsuggestion": "Gợi ý"
},
"wmo": {
- "0-day": "Sunny",
- "0-night": "Clear",
- "1-day": "Mainly Sunny",
- "1-night": "Mainly Clear",
- "2-day": "Partly Cloudy",
- "2-night": "Partly Cloudy",
- "3-day": "Cloudy",
- "3-night": "Cloudy",
- "45-day": "Foggy",
- "45-night": "Foggy",
- "48-day": "Foggy",
- "48-night": "Foggy",
- "51-day": "Light Drizzle",
- "51-night": "Light Drizzle",
- "53-day": "Drizzle",
- "53-night": "Drizzle",
- "55-day": "Heavy Drizzle",
+ "0-day": "Trời nắng",
+ "0-night": "Trời quang",
+ "1-day": "Chủ yếu có nắng",
+ "1-night": "Chủ yếu trời quang",
+ "2-day": "Có mây từng phần",
+ "2-night": "Có mây từng phần",
+ "3-day": "Nhiều mây",
+ "3-night": "Nhiều mây",
+ "45-day": "Có sương mù",
+ "45-night": "Có sương mù",
+ "48-day": "Có sương mù",
+ "48-night": "Có sương mù",
+ "51-day": "Mưa phùn nhẹ",
+ "51-night": "Mưa phùn nhẹ",
+ "53-day": "Mưa phùn",
+ "53-night": "Mưa phùn",
+ "55-day": "Mưa phùn lớn",
"55-night": "Heavy Drizzle",
"56-day": "Light Freezing Drizzle",
"56-night": "Light Freezing Drizzle",
@@ -739,14 +741,14 @@
"m": "m"
},
"atsumeru": {
- "series": "Series",
+ "series": "Bộ truyện",
"archives": "Archives",
"chapters": "Chapters",
"categories": "Categories"
},
"komga": {
"libraries": "Libraries",
- "series": "Series",
+ "series": "Bộ truyện",
"books": "Books"
},
"diskstation": {
@@ -759,9 +761,9 @@
"streams": "Streams"
},
"mylar": {
- "series": "Series",
+ "series": "Phim bộ",
"issues": "Issues",
- "wanted": "Wanted"
+ "wanted": "Cần tải"
},
"photoprism": {
"albums": "Albums",
@@ -844,7 +846,7 @@
"books": "Books",
"authors": "Authors",
"categories": "Categories",
- "series": "Series"
+ "series": "Bộ sách"
},
"booklore": {
"libraries": "Libraries",
@@ -859,7 +861,7 @@
"downloadSpeed": "Speed"
},
"kavita": {
- "seriesCount": "Series",
+ "seriesCount": "Số bộ truyện",
"totalFiles": "Files"
},
"azuredevops": {
@@ -1248,7 +1250,7 @@
"storage": "Storage"
},
"sportarr": {
- "wanted": "Wanted",
+ "wanted": "Cần tải",
"queued": "Queued",
"leagues": "Leagues"
}
diff --git a/public/locales/yue/common.json b/public/locales/yue/common.json
index e21d96624..09fbbb8b3 100644
--- a/public/locales/yue/common.json
+++ b/public/locales/yue/common.json
@@ -39,7 +39,9 @@
"wait": "請稍後"
},
"search": {
- "placeholder": "搜索緊…"
+ "placeholder": "搜索緊…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/zh-Hans/common.json b/public/locales/zh-Hans/common.json
index cda0dcfdf..cca2a9869 100644
--- a/public/locales/zh-Hans/common.json
+++ b/public/locales/zh-Hans/common.json
@@ -39,7 +39,9 @@
"wait": "请稍候"
},
"search": {
- "placeholder": "搜索…"
+ "placeholder": "搜索…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/public/locales/zh-Hant/common.json b/public/locales/zh-Hant/common.json
index fb8699408..97eec8ac7 100644
--- a/public/locales/zh-Hant/common.json
+++ b/public/locales/zh-Hant/common.json
@@ -39,7 +39,9 @@
"wait": "請稍候"
},
"search": {
- "placeholder": "搜尋…"
+ "placeholder": "搜尋…",
+ "provider": "Search engine: {{name}}",
+ "select_provider": "Select search engine"
},
"resources": {
"cpu": "CPU",
diff --git a/src/__tests__/pages/api/auth/[...nextauth].test.js b/src/__tests__/pages/api/auth/[...nextauth].test.js
index e963b3b59..05f670f50 100644
--- a/src/__tests__/pages/api/auth/[...nextauth].test.js
+++ b/src/__tests__/pages/api/auth/[...nextauth].test.js
@@ -73,6 +73,7 @@ describe("pages/api/auth/[...nextauth]", () => {
it("routes sanitized NextAuth logs through the Homepage logger", async () => {
const mod = await import("pages/api/auth/[...nextauth]");
const sensitiveMetadata = {
+ error: Object.assign(new Error("State cookie was missing."), { access_token: "sensitive-access-token" }),
clientSecret: "sensitive-client-secret",
access_token: "sensitive-access-token",
id_token: "sensitive-id-token",
@@ -82,7 +83,7 @@ describe("pages/api/auth/[...nextauth]", () => {
mod.authOptions.logger.warn("NEXTAUTH_URL", sensitiveMetadata);
mod.authOptions.logger.debug("OAUTH_CALLBACK_RESPONSE", sensitiveMetadata);
- expect(errorMock).toHaveBeenCalledWith("%s", "OAUTH_CALLBACK_ERROR");
+ expect(errorMock).toHaveBeenCalledWith("%s: %s", "OAUTH_CALLBACK_ERROR", "State cookie was missing.");
expect(warnMock).toHaveBeenCalledWith("%s", "NEXTAUTH_URL");
expect(debugMock).toHaveBeenCalledWith("%s", "OAUTH_CALLBACK_RESPONSE");
expect(JSON.stringify([...errorMock.mock.calls, ...warnMock.mock.calls, ...debugMock.mock.calls])).not.toContain(
diff --git a/src/__tests__/pages/index.test.jsx b/src/__tests__/pages/index.test.jsx
index 1d8c03240..c543161c7 100644
--- a/src/__tests__/pages/index.test.jsx
+++ b/src/__tests__/pages/index.test.jsx
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
-import { fireEvent, render, screen, waitFor } from "@testing-library/react";
+import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ColorContext } from "utils/contexts/color";
@@ -25,6 +25,7 @@ const {
throwIn: null,
validateData: [],
hashData: null,
+ hashConfig: null,
mutateHash: vi.fn(),
servicesData: [],
bookmarksData: [],
@@ -59,9 +60,12 @@ const {
const serverSideTranslations = vi.fn(async (language) => ({ _translations: language }));
const logger = { error: vi.fn() };
- const useSWR = vi.fn((key) => {
+ const useSWR = vi.fn((key, config) => {
if (key === "/api/validate") return { data: state.validateData };
- if (key === "/api/hash") return { data: state.hashData, mutate: state.mutateHash };
+ if (key === "/api/hash") {
+ state.hashConfig = config;
+ return { data: state.hashData, mutate: state.mutateHash };
+ }
if (key === "/api/services") return { data: state.servicesData };
if (key === "/api/bookmarks") return { data: state.bookmarksData };
if (key === "/api/widgets") return { data: state.widgetsData };
@@ -172,6 +176,7 @@ describe("pages/index getStaticProps", () => {
state.throwIn = null;
state.validateData = [];
state.hashData = null;
+ state.hashConfig = null;
state.servicesData = [];
state.bookmarksData = [];
state.widgetsData = [];
@@ -351,6 +356,7 @@ describe("pages/index Index routing + SWR branches", () => {
}
await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });
+ act(() => state.hashConfig.onSuccess(state.hashData));
await waitFor(() => {
expect(fetchSpy).toHaveBeenCalledWith("/api/revalidate");
@@ -379,11 +385,24 @@ describe("pages/index Index routing + SWR branches", () => {
localStorage.removeItem("hash");
await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });
+ act(() => state.hashConfig.onSuccess(state.hashData));
await waitFor(() => {
expect(localStorage.getItem("hash")).toBe("first-hash");
});
});
+
+ it("ignores a hash response without a hash", async () => {
+ state.validateData = [];
+ localStorage.setItem("hash", "old-hash");
+
+ await renderIndex({ initialSettings: { title: "Homepage", layout: {} }, settings: { layout: {} } });
+
+ expect(() => act(() => state.hashConfig.onSuccess(null))).not.toThrow();
+ expect(() => act(() => state.hashConfig.onSuccess({}))).not.toThrow();
+ expect(localStorage.getItem("hash")).toBe("old-hash");
+ expect(document.querySelector(".animate-spin")).toBeFalsy();
+ });
});
describe("pages/index Home behavior", () => {
@@ -419,11 +438,27 @@ describe("pages/index Home behavior", () => {
fireEvent.keyDown(document.body, { key: "a" });
expect(screen.getByTestId("quicklaunch")).toHaveTextContent("open:3");
+ expect(state.quickLaunchProps.searchString).toBe("a");
fireEvent.keyDown(document.body, { key: "Escape" });
expect(screen.getByTestId("quicklaunch")).toHaveTextContent("closed:3");
});
+ it("opens search on space without seeding the query with whitespace", async () => {
+ await renderIndex({
+ initialSettings: { title: "Homepage", layout: {} },
+ settings: { title: "Homepage", layout: {}, language: "en" },
+ });
+
+ await waitFor(() => {
+ expect(state.quickLaunchProps).toBeTruthy();
+ });
+
+ fireEvent.keyDown(document.body, { key: " " });
+ expect(screen.getByTestId("quicklaunch")).toHaveTextContent("open:3");
+ expect(state.quickLaunchProps.searchString).toBe("");
+ });
+
it("renders services and bookmark groups when present", async () => {
await renderIndex({
initialSettings: { title: "Homepage", layout: {} },
@@ -489,7 +524,7 @@ describe("pages/index Home behavior", () => {
state.widgetsData = [];
const { setTheme, setColor, setSettings } = await renderIndex({
- initialSettings: { title: "Homepage", layout: {} },
+ initialSettings: { title: "Homepage", layout: {}, favicon: "/x.ico" },
settings: {
title: "Homepage",
layout: {},
@@ -498,7 +533,6 @@ describe("pages/index Home behavior", () => {
color: "emerald",
disableIndexing: true,
base: "/base/",
- favicon: "/x.ico",
},
theme: "dark",
color: "slate",
@@ -516,6 +550,20 @@ describe("pages/index Home behavior", () => {
expect(document.querySelector('link[rel="icon"]')?.getAttribute("href")).toBe("/x.ico");
});
+ // Safari reads the head without running JS, so the favicon has to be there before the
+ // settings context is populated, and mask-icon must not be
+ it("renders a custom favicon before the settings context is populated", async () => {
+ await renderIndex({
+ initialSettings: { title: "Homepage", layout: {}, favicon: "/x.ico" },
+ settings: {},
+ });
+
+ expect(document.querySelector('link[rel="icon"]')?.getAttribute("href")).toBe("/x.ico");
+ expect(document.querySelector('link[rel="apple-touch-icon"]')?.getAttribute("href")).toBe("/x.ico");
+ expect(document.querySelector('link[rel="mask-icon"]')).toBeNull();
+ expect(document.querySelector('link[rel="shortcut icon"]')).toBeNull();
+ });
+
it("marks information widgets as right-aligned for known widget types", async () => {
await renderIndex({
initialSettings: { title: "Homepage", layout: {} },
diff --git a/src/components/bookmarks/group.jsx b/src/components/bookmarks/group.jsx
index 88d4d7e8c..3030cfe15 100644
--- a/src/components/bookmarks/group.jsx
+++ b/src/components/bookmarks/group.jsx
@@ -1,10 +1,11 @@
import { Disclosure, Transition } from "@headlessui/react";
import classNames from "classnames";
+import { useEffect, useRef } from "react";
+import { MdKeyboardArrowDown } from "react-icons/md";
+
import List from "components/bookmarks/list";
import ErrorBoundary from "components/errorboundry";
import ResolvedIcon from "components/resolvedicon";
-import { useEffect, useRef } from "react";
-import { MdKeyboardArrowDown } from "react-icons/md";
export default function BookmarksGroup({
bookmarks,
diff --git a/src/components/bookmarks/item.jsx b/src/components/bookmarks/item.jsx
index 7ea16bbaa..6d2c427bc 100644
--- a/src/components/bookmarks/item.jsx
+++ b/src/components/bookmarks/item.jsx
@@ -1,6 +1,7 @@
import classNames from "classnames";
-import ResolvedIcon from "components/resolvedicon";
import { useContext } from "react";
+
+import ResolvedIcon from "components/resolvedicon";
import { SettingsContext } from "utils/contexts/settings";
export default function Item({ bookmark, iconOnly = false }) {
diff --git a/src/components/bookmarks/list.jsx b/src/components/bookmarks/list.jsx
index f24794db9..8f41afdfa 100644
--- a/src/components/bookmarks/list.jsx
+++ b/src/components/bookmarks/list.jsx
@@ -1,8 +1,9 @@
import classNames from "classnames";
-import Item from "components/bookmarks/item";
import { columnMap } from "../../utils/layout/columns";
+import Item from "components/bookmarks/item";
+
export default function List({ bookmarks, layout, bookmarksStyle }) {
let classes = layout?.style === "row" ? `grid ${columnMap[layout?.columns]} gap-x-2` : "flex flex-col bookmark-list";
const style = {};
diff --git a/src/components/favicon.jsx b/src/components/favicon.jsx
index 8961d655e..591a88943 100644
--- a/src/components/favicon.jsx
+++ b/src/components/favicon.jsx
@@ -1,8 +1,8 @@
/* eslint-disable @next/next/no-img-element */
/* eslint-disable jsx-a11y/alt-text */
import { useContext, useEffect, useRef } from "react";
-import { ColorContext } from "utils/contexts/color";
+import { ColorContext } from "utils/contexts/color";
import themes from "utils/styles/themes";
export function Svg({ svgRef = null }) {
diff --git a/src/components/quicklaunch.jsx b/src/components/quicklaunch.jsx
index f89192a66..7375259ce 100644
--- a/src/components/quicklaunch.jsx
+++ b/src/components/quicklaunch.jsx
@@ -3,11 +3,12 @@ import { useTranslation } from "next-i18next/pages";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { FiSearch } from "react-icons/fi";
import useSWR from "swr";
-import { SettingsContext } from "utils/contexts/settings";
import ResolvedIcon from "./resolvedicon";
import { getStoredProvider, searchProviders } from "./widgets/search/search";
+import { SettingsContext } from "utils/contexts/settings";
+
const MOBILE_BUTTON_POSITIONS = {
"top-left": "top-4 left-4",
"top-right": "top-4 right-4",
@@ -15,6 +16,69 @@ const MOBILE_BUTTON_POSITIONS = {
"bottom-right": "bottom-4 right-4",
};
+function parseUrl(searchString) {
+ if (!/.+[.:].+/.test(searchString)) return null; // basic test for probably a url
+
+ try {
+ return new URL(searchString.toLowerCase().startsWith("http") ? searchString : `https://${searchString}`);
+ } catch {
+ return null;
+ }
+}
+
+function getSearchResults({
+ hideVisitURL,
+ searchDescriptions,
+ searchProvider,
+ searchString,
+ searchSuggestions,
+ servicesAndBookmarks,
+ t,
+ url,
+}) {
+ if (searchString.trim().length === 0) return [];
+
+ const results = servicesAndBookmarks.flatMap((result) => {
+ const nameMatch = result.name.toLowerCase().includes(searchString);
+ const descriptionMatch = searchDescriptions && result.description?.toLowerCase().includes(searchString);
+
+ if (!nameMatch && !descriptionMatch) return [];
+ return [{ ...result, ...(searchDescriptions && { priority: nameMatch ? 2 : +descriptionMatch }) }];
+ });
+
+ if (searchDescriptions) {
+ results.sort((a, b) => b.priority - a.priority);
+ }
+
+ if (searchProvider) {
+ results.push({
+ href: searchProvider.url + encodeURIComponent(searchString),
+ name: `${searchProvider.name ?? t("quicklaunch.custom")} ${t("quicklaunch.search")}`,
+ type: "search",
+ });
+
+ if (searchProvider.showSearchSuggestions && searchProvider.suggestionUrl && searchSuggestions[1]) {
+ results.push(
+ ...searchSuggestions[1].map((suggestion) => ({
+ href: searchProvider.url + encodeURIComponent(suggestion),
+ name: suggestion,
+ type: "searchSuggestion",
+ })),
+ );
+ }
+ }
+
+ if (!hideVisitURL && url) {
+ results.unshift({
+ href: url.toString(),
+ name: `${t("quicklaunch.visit")} URL`,
+ type: "url",
+ });
+ }
+
+ return results;
+}
+
export default function QuickLaunch({ servicesAndBookmarks, searchString, setSearchString, isOpen, setSearching }) {
const { t } = useTranslation();
@@ -23,9 +87,8 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
const searchField = useRef();
- const [results, setResults] = useState([]);
const [currentItemIndex, setCurrentItemIndex] = useState(null);
- const [url, setUrl] = useState(null);
+ const url = parseUrl(searchString);
const [searchSuggestions, setSearchSuggestions] = useState([]);
const { data: widgets } = useSWR("/api/widgets");
@@ -62,7 +125,7 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
: null;
function openCurrentItem(newWindow) {
- const result = results[currentItemIndex];
+ const result = results[activeItemIndex];
window.open(
result.href,
newWindow ? "_blank" : (result.target ?? searchProvider?.target ?? settings.target ?? "_blank"),
@@ -81,17 +144,9 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
function handleSearchChange(event) {
const rawSearchString = event.target.value;
- try {
- if (!/.+[.:].+/g.test(rawSearchString)) throw new Error(); // basic test for probably a url
- let urlString = rawSearchString;
- if (urlString.toLowerCase().indexOf("http") !== 0) urlString = `https://${rawSearchString}`;
- setUrl(new URL(urlString)); // basic validation
- setSearchString(rawSearchString);
- return;
- } catch (e) {
- setUrl(null);
- }
- setSearchString(rawSearchString.toLowerCase());
+ setCurrentItemIndex(null);
+ // urls keep their casing, everything else is lowercased for matching
+ setSearchString(parseUrl(rawSearchString) ? rawSearchString : rawSearchString.toLowerCase());
}
function handleSearchKeyDown(event) {
@@ -103,18 +158,19 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
} else if (event.key === "Enter" && results.length) {
closeAndReset();
openCurrentItem(event.metaKey);
- } else if (event.key === "ArrowDown" && results[currentItemIndex + 1]) {
- setCurrentItemIndex(currentItemIndex + 1);
+ } else if (event.key === "ArrowDown" && results[activeItemIndex + 1]) {
+ setCurrentItemIndex(activeItemIndex + 1);
event.preventDefault();
- } else if (event.key === "ArrowUp" && currentItemIndex > 0) {
- setCurrentItemIndex(currentItemIndex - 1);
+ } else if (event.key === "ArrowUp" && activeItemIndex > 0) {
+ setCurrentItemIndex(activeItemIndex - 1);
event.preventDefault();
} else if (
event.key === "ArrowRight" &&
- results[currentItemIndex] &&
- results[currentItemIndex].type === "searchSuggestion"
+ results[activeItemIndex] &&
+ results[activeItemIndex].type === "searchSuggestion"
) {
- setSearchString(results[currentItemIndex].name);
+ setCurrentItemIndex(null);
+ setSearchString(results[activeItemIndex].name);
}
}
@@ -138,88 +194,51 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
}
useEffect(() => {
- const abortController = new AbortController();
-
- if (searchString.trim().length === 0) setResults([]);
- else {
- let newResults = servicesAndBookmarks.filter((r) => {
- const nameMatch = r.name.toLowerCase().includes(searchString);
- let descriptionMatch;
- if (searchDescriptions) {
- descriptionMatch = r.description?.toLowerCase().includes(searchString);
- r.priority = nameMatch ? 2 * +nameMatch : +descriptionMatch;
- }
- return nameMatch || descriptionMatch;
- });
-
- if (searchDescriptions) {
- newResults = newResults.sort((a, b) => b.priority - a.priority);
- }
-
- if (searchProvider) {
- newResults.push({
- href: searchProvider.url + encodeURIComponent(searchString),
- name: `${searchProvider.name ?? t("quicklaunch.custom")} ${t("quicklaunch.search")}`,
- type: "search",
- });
-
- if (searchProvider.showSearchSuggestions && searchProvider.suggestionUrl) {
- if (searchString.trim() !== searchSuggestions[0]?.trim()) {
- fetch(
- `/api/search/searchSuggestion?query=${encodeURIComponent(searchString)}&providerName=${
- searchProvider.name ?? "Custom"
- }`,
- { signal: abortController.signal },
- )
- .then(async (searchSuggestionResult) => {
- const newSearchSuggestions = await searchSuggestionResult.json();
-
- if (newSearchSuggestions) {
- if (newSearchSuggestions[1].length > 4) {
- newSearchSuggestions[1] = newSearchSuggestions[1].splice(0, 4);
- }
- setSearchSuggestions(newSearchSuggestions);
- }
- })
- .catch(() => {
- // If there is an error, just ignore it. There just will be no search suggestions.
- });
- }
-
- if (searchSuggestions[1]) {
- newResults = newResults.concat(
- searchSuggestions[1].map((suggestion) => ({
- href: searchProvider.url + encodeURIComponent(suggestion),
- name: suggestion,
- type: "searchSuggestion",
- })),
- );
- }
- }
- }
-
- if (!hideVisitURL && url) {
- newResults.unshift({
- href: url.toString(),
- name: `${t("quicklaunch.visit")} URL`,
- type: "url",
- });
- }
-
- setResults(newResults);
-
- if (newResults.length) {
- setCurrentItemIndex(0);
- }
+ if (
+ searchString.trim().length === 0 ||
+ !searchProvider?.showSearchSuggestions ||
+ !searchProvider.suggestionUrl ||
+ searchString.trim() === searchSuggestions[0]?.trim()
+ ) {
+ return undefined;
}
- return () => {
- abortController.abort();
- };
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [searchString, servicesAndBookmarks, searchDescriptions, hideVisitURL, searchSuggestions, searchProvider, url]);
+ const abortController = new AbortController();
+
+ fetch(
+ `/api/search/searchSuggestion?query=${encodeURIComponent(searchString)}&providerName=${
+ searchProvider.name ?? "Custom"
+ }`,
+ { signal: abortController.signal },
+ )
+ .then(async (searchSuggestionResult) => {
+ const newSearchSuggestions = await searchSuggestionResult.json();
+
+ if (newSearchSuggestions) {
+ setCurrentItemIndex(null);
+ setSearchSuggestions([newSearchSuggestions[0], newSearchSuggestions[1].slice(0, 4)]);
+ }
+ })
+ .catch(() => {
+ // If there is an error, just ignore it. There just will be no search suggestions.
+ });
+
+ return () => abortController.abort();
+ }, [searchProvider, searchString, searchSuggestions]);
+
+ const results = getSearchResults({
+ hideVisitURL,
+ searchDescriptions,
+ searchProvider,
+ searchString,
+ searchSuggestions,
+ servicesAndBookmarks,
+ t,
+ url,
+ });
+
+ const activeItemIndex = currentItemIndex ?? (results.length ? 0 : null);
- const [hidden, setHidden] = useState(true);
useEffect(() => {
function handleBackdropClick(event) {
if (event.target?.tagName === "DIV") closeAndReset();
@@ -228,14 +247,11 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
if (isOpen) {
searchField.current.focus();
document.body.addEventListener("click", handleBackdropClick);
- setHidden(false);
} else {
- document.body.removeEventListener("click", handleBackdropClick);
searchField.current.blur();
- setTimeout(() => {
- setHidden(true);
- }, 300); // disable on close
}
+
+ return () => document.body.removeEventListener("click", handleBackdropClick);
}, [isOpen, closeAndReset]);
function highlightText(text) {
@@ -259,13 +275,17 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
<>
@@ -297,7 +317,7 @@ export default function QuickLaunch({ servicesAndBookmarks, searchString, setSea
onKeyDown={handleItemKeyDown}
className={classNames(
"flex flex-row w-full items-center justify-between rounded-md text-sm md:text-xl py-2 px-4 cursor-pointer text-theme-700 dark:text-theme-200",
- i === currentItemIndex && "bg-theme-300/50 dark:bg-theme-700/50",
+ i === activeItemIndex && "bg-theme-300/50 dark:bg-theme-700/50",
)}
>
diff --git a/src/components/quicklaunch.test.jsx b/src/components/quicklaunch.test.jsx
index 65cf7a508..031471916 100644
--- a/src/components/quicklaunch.test.jsx
+++ b/src/components/quicklaunch.test.jsx
@@ -46,13 +46,25 @@ function Wrapper({ servicesAndBookmarks = [], initialOpen = true } = {}) {
const [isOpen, setSearching] = useState(initialOpen);
return (
-
+ <>
+
+
+ >
);
}
@@ -151,6 +163,35 @@ describe("components/quicklaunch", () => {
openSpy.mockRestore();
});
+ it("does not carry a previous url result into a search seeded by a keypress", async () => {
+ renderWithProviders(
, {
+ settings: {
+ target: "_self",
+ quicklaunch: {
+ provider: "duckduckgo",
+ showSearchSuggestions: false,
+ },
+ },
+ });
+
+ const input = screen.getByPlaceholderText("Search");
+ await waitFor(() => expect(input).toHaveFocus());
+
+ fireEvent.change(input, { target: { value: "example.com" } });
+ expect(await screen.findByText("quicklaunch.visit URL")).toBeInTheDocument();
+
+ fireEvent.keyDown(input, { key: "Escape" });
+ await act(async () => {
+ await new Promise((r) => setTimeout(r, 350));
+ });
+
+ // reopen by "typing" a character, the way pages/index does
+ fireEvent.click(screen.getByTestId("seed-key"));
+
+ expect(input).toHaveValue("g");
+ expect(screen.queryByText("quicklaunch.visit URL")).not.toBeInTheDocument();
+ });
+
it("closes on Escape and clears the search string after the timeout", async () => {
renderWithProviders(
, {
settings: {
diff --git a/src/components/resolvedicon.jsx b/src/components/resolvedicon.jsx
index 77ba91191..cf5fde02f 100644
--- a/src/components/resolvedicon.jsx
+++ b/src/components/resolvedicon.jsx
@@ -1,5 +1,6 @@
import Image from "next/image";
import { useContext } from "react";
+
import { SettingsContext } from "utils/contexts/settings";
import { ThemeContext } from "utils/contexts/theme";
diff --git a/src/components/services/group.jsx b/src/components/services/group.jsx
index 403f6063c..881da5b7a 100644
--- a/src/components/services/group.jsx
+++ b/src/components/services/group.jsx
@@ -1,12 +1,13 @@
import { Disclosure, Transition } from "@headlessui/react";
import classNames from "classnames";
-import ResolvedIcon from "components/resolvedicon";
-import List from "components/services/list";
import { useEffect, useRef } from "react";
import { MdKeyboardArrowDown } from "react-icons/md";
import { columnMap } from "../../utils/layout/columns";
+import ResolvedIcon from "components/resolvedicon";
+import List from "components/services/list";
+
export default function ServicesGroup({
group,
layout,
diff --git a/src/components/services/item.jsx b/src/components/services/item.jsx
index c23e16a23..88c6383ae 100644
--- a/src/components/services/item.jsx
+++ b/src/components/services/item.jsx
@@ -1,10 +1,5 @@
import classNames from "classnames";
-import ResolvedIcon from "components/resolvedicon";
import { useContext, useState } from "react";
-import { SettingsContext } from "utils/contexts/settings";
-import Docker from "widgets/docker/component";
-import Kubernetes from "widgets/kubernetes/component";
-import ProxmoxVM from "widgets/proxmoxvm/component";
import KubernetesStatus from "./kubernetes-status";
import Ping from "./ping";
@@ -13,6 +8,12 @@ import SiteMonitor from "./site-monitor";
import Status from "./status";
import Widget from "./widget";
+import ResolvedIcon from "components/resolvedicon";
+import { SettingsContext } from "utils/contexts/settings";
+import Docker from "widgets/docker/component";
+import Kubernetes from "widgets/kubernetes/component";
+import ProxmoxVM from "widgets/proxmoxvm/component";
+
export default function Item({ service, groupName, useEqualHeights }) {
const hasLink = service.href && service.href !== "#";
const { settings } = useContext(SettingsContext);
diff --git a/src/components/services/list.jsx b/src/components/services/list.jsx
index 1acf94d62..63fcea388 100644
--- a/src/components/services/list.jsx
+++ b/src/components/services/list.jsx
@@ -1,8 +1,9 @@
import classNames from "classnames";
-import Item from "components/services/item";
import { columnMap } from "../../utils/layout/columns";
+import Item from "components/services/item";
+
export default function List({ groupName, services, layout, useEqualHeights, header }) {
return (
{
- const dateFormat = new Intl.DateTimeFormat(dateLocale, { ...format });
- setDate(dateFormat.format(new Date()));
- const interval = setInterval(() => {
- setDate(dateFormat.format(new Date()));
- }, 1000);
- return () => clearInterval(interval);
- }, [date, setDate, dateLocale, format]);
+ const currentTime = useCurrentTime(1000);
+ const dateFormatter = useMemo(() => new Intl.DateTimeFormat(dateLocale, { ...format }), [dateLocale, format]);
+ const date = currentTime === null ? "" : dateFormatter.format(new Date(currentTime));
return (
diff --git a/src/components/widgets/glances/glances.jsx b/src/components/widgets/glances/glances.jsx
index f8dd643cf..0a9aec390 100644
--- a/src/components/widgets/glances/glances.jsx
+++ b/src/components/widgets/glances/glances.jsx
@@ -4,13 +4,14 @@ import { useContext } from "react";
import { FaMemory, FaRegClock, FaThermometerHalf } from "react-icons/fa";
import { FiCpu, FiHardDrive } from "react-icons/fi";
import useSWR from "swr";
-import { SettingsContext } from "utils/contexts/settings";
import Error from "../widget/error";
import Resource from "../widget/resource";
import Resources from "../widget/resources";
import WidgetLabel from "../widget/widget_label";
+import { SettingsContext } from "utils/contexts/settings";
+
const defaultCpuSensorLabels = ["cpu_thermal", "Core", "Tctl", "Temperature"];
function convertToFahrenheit(t) {
diff --git a/src/components/widgets/logo/logo.jsx b/src/components/widgets/logo/logo.jsx
index 1446fdf64..da033048f 100644
--- a/src/components/widgets/logo/logo.jsx
+++ b/src/components/widgets/logo/logo.jsx
@@ -1,8 +1,8 @@
-import ResolvedIcon from "components/resolvedicon";
-
import Container from "../widget/container";
import Raw from "../widget/raw";
+import ResolvedIcon from "components/resolvedicon";
+
export default function Logo({ options }) {
return (
{
it("normalizes options.network=true to default interfaceName in the request", () => {
useSWR.mockReturnValue({ data: undefined, error: undefined });
+ const options = Object.freeze({ network: true });
- render();
+ render();
expect(useSWR).toHaveBeenCalledWith(expect.stringContaining("interfaceName=default"), expect.any(Object));
+ expect(options.network).toBe(true);
});
it("renders rates and usage percentage when data is present", () => {
diff --git a/src/components/widgets/search/search.jsx b/src/components/widgets/search/search.jsx
index 3fb810a92..486b89ccf 100644
--- a/src/components/widgets/search/search.jsx
+++ b/src/components/widgets/search/search.jsx
@@ -11,7 +11,7 @@ import {
} from "@headlessui/react";
import classNames from "classnames";
import { useTranslation } from "next-i18next/pages";
-import { Fragment, useEffect, useMemo, useState } from "react";
+import { Fragment, useEffect, useMemo, useState, useSyncExternalStore } from "react";
import { BiLogoBing } from "react-icons/bi";
import { FiSearch } from "react-icons/fi";
import { SiBaidu, SiBrave, SiDuckduckgo, SiGoogle } from "react-icons/si";
@@ -79,24 +79,32 @@ export function getStoredProvider() {
return null;
}
+function subscribeToStoredProvider(onStoreChange) {
+ const handleStorage = (event) => {
+ if (event.key === localStorageKey) onStoreChange();
+ };
+
+ window.addEventListener("storage", handleStorage);
+ return () => window.removeEventListener("storage", handleStorage);
+}
+
export default function Search({ options }) {
const { t } = useTranslation();
// options is a fresh object each render, so memo on provider itself
const availableProviderIds = useMemo(() => getAvailableProviderIds(options.provider) ?? [], [options.provider]);
+ const storedProvider = useSyncExternalStore(subscribeToStoredProvider, getStoredProvider, () => null);
+ const storedProviderId = Object.keys(searchProviders).find(
+ (providerId) => searchProviders[providerId] === storedProvider,
+ );
+ const initialProvider = availableProviderIds.includes(storedProviderId)
+ ? storedProvider
+ : searchProviders[availableProviderIds[0] ?? "google"];
const [query, setQuery] = useState("");
- const [selectedProvider, setSelectedProvider] = useState(searchProviders[availableProviderIds[0] ?? "google"]);
+ const [providerOverride, setProviderOverride] = useState(null);
const [searchSuggestions, setSearchSuggestions] = useState([]);
-
- useEffect(() => {
- const storedProvider = getStoredProvider();
- let storedProviderKey = null;
- storedProviderKey = Object.keys(searchProviders).find((pkey) => searchProviders[pkey] === storedProvider);
- if (storedProvider && availableProviderIds.includes(storedProviderKey)) {
- setSelectedProvider(storedProvider);
- }
- }, [availableProviderIds]);
+ const selectedProvider = providerOverride ?? initialProvider;
useEffect(() => {
const abortController = new AbortController();
@@ -130,8 +138,6 @@ export default function Search({ options }) {
};
}, [selectedProvider, options, query, searchSuggestions]);
- let currentSuggestion;
-
function doSearch(value) {
const q = encodeURIComponent(value);
const { url } = selectedProvider;
@@ -142,13 +148,11 @@ export default function Search({ options }) {
}
setQuery("");
- currentSuggestion = null;
}
const handleSearchKeyDown = (event) => {
- const useSuggestion = searchSuggestions.length && currentSuggestion;
- if (event.key === "Enter") {
- doSearch(useSuggestion ? currentSuggestion : event.target.value);
+ if (event.key === "Enter" && !searchSuggestions[1]?.length) {
+ doSearch(event.target.value);
}
};
@@ -157,7 +161,7 @@ export default function Search({ options }) {
}
const onChangeProvider = (provider) => {
- setSelectedProvider(provider);
+ setProviderOverride(provider);
localStorage.setItem(localStorageKey, provider.name);
};
@@ -166,7 +170,7 @@ export default function Search({ options }) {
-
+
{
setQuery(event.target.value);
}}
@@ -204,8 +209,8 @@ export default function Search({ options }) {
bg-theme-600/40 dark:bg-white/10
focus:ring-theme-500 dark:focus:ring-white/50"
>
-
- {t("search.search")}
+
+ {t("search.provider", { name: selectedProvider.name })}
-
+
+ {p.name}
)}
@@ -248,32 +255,24 @@ export default function Search({ options }) {
{searchSuggestions[1]?.length > 0 && (
-
+
+ {query}
+
{searchSuggestions[1].map((suggestion) => (
-
{
- doSearch(suggestion);
- }}
- className="flex w-full"
- >
- {({ active }) => {
- if (active) currentSuggestion = suggestion;
- return (
-
- {suggestion.indexOf(query) === 0 ? query : ""}
-
- {suggestion.indexOf(query) === 0 ? suggestion.substring(query.length) : suggestion}
-
-
- );
- }}
+
+ {({ active }) => (
+
+ {suggestion.indexOf(query) === 0 ? query : ""}
+
+ {suggestion.indexOf(query) === 0 ? suggestion.substring(query.length) : suggestion}
+
+
+ )}
))}
diff --git a/src/components/widgets/search/search.test.jsx b/src/components/widgets/search/search.test.jsx
index cbb603884..6c0fb0328 100644
--- a/src/components/widgets/search/search.test.jsx
+++ b/src/components/widgets/search/search.test.jsx
@@ -9,6 +9,7 @@ import { renderWithProviders } from "test-utils/render-with-providers";
vi.mock("@headlessui/react", async () => {
const React = await import("react");
const { Fragment, createContext, useContext } = React;
+ const ComboboxContext = createContext(null);
const ListboxContext = createContext(null);
function passthrough({ as: As = "div", children, ...props }) {
@@ -18,9 +19,21 @@ vi.mock("@headlessui/react", async () => {
}
return {
- Combobox: passthrough,
+ Combobox: ({ onChange, children }) => (
+
+ {children}
+
+ ),
ComboboxInput: (props) => ,
- ComboboxOption: passthrough,
+ ComboboxOption: ({ as: As = "div", value, children, ...props }) => {
+ const ctx = useContext(ComboboxContext);
+ const content = typeof children === "function" ? children({ active: false }) : children;
+ return (
+ ctx?.onChange?.(value)} {...props}>
+ {content}
+
+ );
+ },
ComboboxOptions: passthrough,
Listbox: ({ value, onChange, children, ...props }) => (
@@ -165,6 +178,28 @@ describe("components/widgets/search", () => {
openSpy.mockRestore();
});
+ it("gives every provider option an accessible name", () => {
+ renderWithProviders(
+ ,
+ { settings: {} },
+ );
+
+ expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual([
+ "Google",
+ "DuckDuckGo",
+ "Brave",
+ ]);
+ });
+
+ it("labels the search input and the provider button", () => {
+ renderWithProviders(, {
+ settings: {},
+ });
+
+ expect(screen.getByRole("textbox")).toHaveAccessibleName("search.placeholder");
+ expect(screen.getByRole("button")).toHaveAccessibleName(/search\.provider/);
+ });
+
it("fetches search suggestions and triggers a search when a suggestion is selected", async () => {
const openSpy = vi.spyOn(window, "open").mockImplementation(() => null);
diff --git a/src/components/widgets/unifi_console/unifi_console.jsx b/src/components/widgets/unifi_console/unifi_console.jsx
index ed16f2b53..05bc8d49f 100644
--- a/src/components/widgets/unifi_console/unifi_console.jsx
+++ b/src/components/widgets/unifi_console/unifi_console.jsx
@@ -14,11 +14,11 @@ import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Widget({ options }) {
const { t } = useTranslation();
- options.service_group = options.service_name = "unifi_console";
- const { data: statsData, error: statsError } = useWidgetAPI(options, "stat/sites", { index: options.index });
+ const widgetOptions = { ...options, service_group: "unifi_console", service_name: "unifi_console" };
+ const { data: statsData, error: statsError } = useWidgetAPI(widgetOptions, "stat/sites", { index: options.index });
if (statsError) {
- return ;
+ return ;
}
const defaultSite = options.site
@@ -27,27 +27,27 @@ export default function Widget({ options }) {
if (!defaultSite) {
return (
-
+
{t("unifi.wait")}
);
}
- const wan = defaultSite.health.find((h) => h.subsystem === "wan");
- const lan = defaultSite.health.find((h) => h.subsystem === "lan");
- const wlan = defaultSite.health.find((h) => h.subsystem === "wlan");
- [wan, lan, wlan].forEach((s) => {
- s.up = s.status === "ok";
- s.show = s.status !== "unknown";
- });
+ const getHealth = (subsystem) => {
+ const health = defaultSite.health.find((item) => item.subsystem === subsystem) ?? { status: "unknown" };
+ return { ...health, up: health.status === "ok", show: health.status !== "unknown" };
+ };
+ const wan = getHealth("wan");
+ const lan = getHealth("lan");
+ const wlan = getHealth("wlan");
const name = wan.gw_name ?? defaultSite.desc;
const uptime = wan["gw_system-stats"] ? wan["gw_system-stats"].uptime : null;
const dataEmpty = !(wan.show || lan.show || wlan.show || uptime);
return (
-
+
diff --git a/src/components/widgets/unifi_console/unifi_console.test.jsx b/src/components/widgets/unifi_console/unifi_console.test.jsx
index c447a756f..0dc16b546 100644
--- a/src/components/widgets/unifi_console/unifi_console.test.jsx
+++ b/src/components/widgets/unifi_console/unifi_console.test.jsx
@@ -40,32 +40,41 @@ describe("components/widgets/unifi_console", () => {
it("renders a wait state when no site is available yet", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+ const options = Object.freeze({ index: 0 });
- renderWithProviders(
, { settings: { target: "_self" } });
+ renderWithProviders(
, { settings: { target: "_self" } });
expect(screen.getByText("unifi.wait")).toBeInTheDocument();
+ expect(useWidgetAPI).toHaveBeenCalledWith(
+ { index: 0, service_group: "unifi_console", service_name: "unifi_console" },
+ "stat/sites",
+ { index: 0 },
+ );
+ expect(options).toEqual({ index: 0 });
});
it("renders site name and uptime when data is available", () => {
+ const data = {
+ data: [
+ {
+ name: "default",
+ desc: "Home",
+ health: [
+ {
+ subsystem: "wan",
+ status: "ok",
+ gw_name: "Router",
+ "gw_system-stats": { uptime: 172800 },
+ },
+ { subsystem: "lan", status: "unknown" },
+ { subsystem: "wlan", status: "unknown" },
+ ],
+ },
+ ],
+ };
+ const originalData = structuredClone(data);
useWidgetAPI.mockReturnValue({
- data: {
- data: [
- {
- name: "default",
- desc: "Home",
- health: [
- {
- subsystem: "wan",
- status: "ok",
- gw_name: "Router",
- "gw_system-stats": { uptime: 172800 },
- },
- { subsystem: "lan", status: "unknown" },
- { subsystem: "wlan", status: "unknown" },
- ],
- },
- ],
- },
+ data,
error: undefined,
});
@@ -75,6 +84,7 @@ describe("components/widgets/unifi_console", () => {
// common.number is mocked to return the numeric value as a string.
expect(screen.getByText("2")).toBeInTheDocument();
expect(screen.getByText("unifi.days")).toBeInTheDocument();
+ expect(data).toEqual(originalData);
});
it("selects a site by description when options.site is set", () => {
diff --git a/src/components/widgets/widget.jsx b/src/components/widgets/widget.jsx
index ebc706ace..c7f0bf4df 100644
--- a/src/components/widgets/widget.jsx
+++ b/src/components/widgets/widget.jsx
@@ -1,6 +1,7 @@
-import ErrorBoundary from "components/errorboundry";
import dynamic from "next/dynamic";
+import ErrorBoundary from "components/errorboundry";
+
const widgetMappings = {
weatherapi: dynamic(() => import("components/widgets/weather/weather")),
openweathermap: dynamic(() => import("components/widgets/openweathermap/weather")),
diff --git a/src/components/widgets/widget/container.jsx b/src/components/widgets/widget/container.jsx
index 6c48b9209..117d79bf1 100644
--- a/src/components/widgets/widget/container.jsx
+++ b/src/components/widgets/widget/container.jsx
@@ -1,12 +1,13 @@
import classNames from "classnames";
import { useContext } from "react";
-import { SettingsContext } from "utils/contexts/settings";
import PrimaryText from "./primary_text";
import Raw from "./raw";
import SecondaryText from "./secondary_text";
import WidgetIcon from "./widget_icon";
+import { SettingsContext } from "utils/contexts/settings";
+
export function getAllClasses(options, additionalClassNames = "") {
if (options?.style?.header === "boxedWidgets") {
if (options?.style?.cardBlur !== undefined) {
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
index 809e7b216..7c7216660 100644
--- a/src/pages/_app.jsx
+++ b/src/pages/_app.jsx
@@ -5,13 +5,14 @@ import "styles/globals.css";
import "styles/manrope.css";
import "styles/theme.css";
import { SWRConfig } from "swr";
+
+import nextI18nextConfig from "../../next-i18next.config";
+
import { ColorProvider } from "utils/contexts/color";
import { SettingsProvider } from "utils/contexts/settings";
import { TabProvider } from "utils/contexts/tab";
import { ThemeProvider } from "utils/contexts/theme";
-import nextI18nextConfig from "../../next-i18next.config";
-
const tailwindSafelist = [
// TODO: remove pending https://github.com/tailwindlabs/tailwindcss/pull/17147
"backdrop-blur",
diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js
index ce5a9f505..5379f8bbd 100644
--- a/src/pages/api/auth/[...nextauth].js
+++ b/src/pages/api/auth/[...nextauth].js
@@ -71,6 +71,16 @@ function logFailedPasswordSignIn() {
createLogger("nextauth").warn("Failed password sign-in attempt");
}
+function logNextAuthError(code, metadata) {
+ const error = metadata instanceof Error ? metadata : metadata?.error;
+
+ if (error?.message) {
+ createLogger("nextauth").error("%s: %s", code, error.message);
+ } else {
+ createLogger("nextauth").error("%s", code);
+ }
+}
+
let providers = [];
if (authEnabled) {
if (hasOidcConfig) {
@@ -140,7 +150,7 @@ export const authOptions = {
signIn: "/auth/signin",
},
logger: {
- error: (code) => createLogger("nextauth").error("%s", code),
+ error: logNextAuthError,
warn: (code) => createLogger("nextauth").warn("%s", code),
debug: (code) => createLogger("nextauth").debug("%s", code),
},
diff --git a/src/pages/api/search/searchSuggestion.js b/src/pages/api/search/searchSuggestion.js
index f03759c2c..7d291b281 100644
--- a/src/pages/api/search/searchSuggestion.js
+++ b/src/pages/api/search/searchSuggestion.js
@@ -1,5 +1,4 @@
import { searchProviders } from "components/widgets/search/search";
-
import { getSettings } from "utils/config/config";
import { widgetsFromConfig } from "utils/config/widget-helpers";
import { cachedRequest } from "utils/proxy/http";
diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js
index 8aab61fd7..123794854 100644
--- a/src/pages/api/services/proxy.js
+++ b/src/pages/api/services/proxy.js
@@ -63,6 +63,8 @@ export default async function handler(req, res) {
if (serviceProxyHandler instanceof Function) {
// quick return for no endpoint services, calendar is an exception
if (!req.query.endpoint || serviceProxyHandler === calendarProxyHandler) {
+ req.method = "GET";
+ req.body = undefined;
return await serviceProxyHandler(req, res);
}
@@ -86,7 +88,7 @@ export default async function handler(req, res) {
}
req.method = mapping?.method || "GET";
- if (mapping?.body) req.body = mapping?.body;
+ req.body = mapping?.body;
req.query.endpoint = endpoint;
if (mapping.segments || req.query.segments) {
@@ -125,6 +127,8 @@ export default async function handler(req, res) {
if (widget.allowedEndpoints instanceof RegExp) {
if (widget.allowedEndpoints.test(req.query.endpoint)) {
+ req.method = "GET";
+ req.body = undefined;
return await serviceProxyHandler(req, res);
}
}
diff --git a/src/pages/api/services/proxy.test.js b/src/pages/api/services/proxy.test.js
new file mode 100644
index 000000000..3c5336040
--- /dev/null
+++ b/src/pages/api/services/proxy.test.js
@@ -0,0 +1,229 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import createMockRes from "test-utils/create-mock-res";
+
+const { getServiceWidget, logger, genericHandler, allowedHandler, mappedHandler, calendarHandler } = vi.hoisted(() => ({
+ getServiceWidget: vi.fn(),
+ logger: { debug: vi.fn(), error: vi.fn() },
+ genericHandler: vi.fn(),
+ allowedHandler: vi.fn(),
+ mappedHandler: vi.fn(),
+ calendarHandler: vi.fn(),
+}));
+
+vi.mock("utils/logger", () => ({
+ default: () => logger,
+}));
+vi.mock("utils/config/service-helpers", () => ({
+ default: getServiceWidget,
+}));
+vi.mock("widgets/calendar/proxy", () => ({
+ default: calendarHandler,
+}));
+
+vi.mock("widgets/widgets", () => ({
+ default: {
+ noendpoint: {
+ api: "{url}",
+ proxyHandler: genericHandler,
+ },
+ allowed: {
+ api: "{url}/api/{endpoint}",
+ proxyHandler: allowedHandler,
+ allowedEndpoints: /^status$/,
+ },
+ mapped: {
+ api: "{url}/{endpoint}",
+ proxyHandler: mappedHandler,
+ mappings: {
+ Sessions: {
+ endpoint: "Sessions",
+ },
+ Pause: {
+ method: "POST",
+ endpoint: "Sessions/Playing/Pause",
+ body: "mapping-body",
+ },
+ Refresh: {
+ method: "POST",
+ endpoint: "Sessions/Refresh",
+ },
+ },
+ },
+ ical: {
+ api: "{url}",
+ proxyHandler: calendarHandler,
+ },
+ },
+}));
+
+import handler from "./proxy";
+
+function createReq({ method = "GET", body, query = {} } = {}) {
+ return { method, body, query };
+}
+
+describe("pages/api/services/proxy", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("client request shaping", () => {
+ it("forces GET and drops the body on the no-endpoint quick return", async () => {
+ getServiceWidget.mockResolvedValue({ type: "noendpoint", url: "http://example" });
+
+ const req = createReq({
+ method: "POST",
+ body: '{"cmd":"delete_all"}',
+ query: { group: "g", service: "svc", index: "0" },
+ });
+
+ await handler(req, createMockRes());
+
+ expect(genericHandler).toHaveBeenCalledTimes(1);
+ const forwarded = genericHandler.mock.calls[0][0];
+ expect(forwarded.method).toBe("GET");
+ expect(forwarded.body).toBeUndefined();
+ });
+
+ it.each(["PUT", "DELETE", "PATCH"])("forces GET for a %s on the no-endpoint quick return", async (method) => {
+ getServiceWidget.mockResolvedValue({ type: "noendpoint", url: "http://example" });
+
+ const req = createReq({ method, body: "payload", query: { group: "g", service: "svc", index: "0" } });
+
+ await handler(req, createMockRes());
+
+ const forwarded = genericHandler.mock.calls[0][0];
+ expect(forwarded.method).toBe("GET");
+ expect(forwarded.body).toBeUndefined();
+ });
+
+ it("forces GET and drops the body on the allowedEndpoints path", async () => {
+ getServiceWidget.mockResolvedValue({ type: "allowed", url: "http://example" });
+
+ const req = createReq({
+ method: "DELETE",
+ body: "payload",
+ query: { group: "g", service: "svc", endpoint: "status", index: "0" },
+ });
+
+ await handler(req, createMockRes());
+
+ expect(allowedHandler).toHaveBeenCalledTimes(1);
+ const forwarded = allowedHandler.mock.calls[0][0];
+ expect(forwarded.method).toBe("GET");
+ expect(forwarded.body).toBeUndefined();
+ });
+
+ it("forces GET and drops the body on the calendar exception path", async () => {
+ getServiceWidget.mockResolvedValue({ type: "calendar", url: "http://example" });
+
+ const req = createReq({
+ method: "POST",
+ body: "payload",
+ query: { group: "g", service: "svc", endpoint: "events", index: "0" },
+ });
+
+ await handler(req, createMockRes());
+
+ expect(calendarHandler).toHaveBeenCalledTimes(1);
+ const forwarded = calendarHandler.mock.calls[0][0];
+ expect(forwarded.method).toBe("GET");
+ expect(forwarded.body).toBeUndefined();
+ });
+
+ it("forces GET for a mapping that does not declare a method", async () => {
+ getServiceWidget.mockResolvedValue({ type: "mapped", url: "http://example" });
+
+ const req = createReq({
+ method: "POST",
+ body: "payload",
+ query: { group: "g", service: "svc", endpoint: "Sessions", index: "0" },
+ });
+
+ await handler(req, createMockRes());
+
+ const forwarded = mappedHandler.mock.calls[0][0];
+ expect(forwarded.method).toBe("GET");
+ expect(forwarded.body).toBeUndefined();
+ });
+
+ it("drops a client body when a non-GET mapping does not declare one", async () => {
+ getServiceWidget.mockResolvedValue({ type: "mapped", url: "http://example" });
+
+ const req = createReq({
+ method: "POST",
+ body: "attacker-controlled",
+ query: { group: "g", service: "svc", endpoint: "Refresh", index: "0" },
+ });
+
+ await handler(req, createMockRes());
+
+ const forwarded = mappedHandler.mock.calls[0][0];
+ expect(forwarded.method).toBe("POST");
+ expect(forwarded.body).toBeUndefined();
+ });
+ });
+
+ describe("mapping opt-in", () => {
+ it("still allows a mapping to declare a non-GET method and body", async () => {
+ getServiceWidget.mockResolvedValue({ type: "mapped", url: "http://example" });
+
+ const req = createReq({
+ method: "POST",
+ query: { group: "g", service: "svc", endpoint: "Pause", index: "0" },
+ });
+
+ await handler(req, createMockRes());
+
+ expect(mappedHandler).toHaveBeenCalledTimes(1);
+ const forwarded = mappedHandler.mock.calls[0][0];
+ expect(forwarded.method).toBe("POST");
+ expect(forwarded.body).toBe("mapping-body");
+ expect(forwarded.query.endpoint).toBe("Sessions/Playing/Pause");
+ });
+
+ it("rejects a method that does not match the mapping", async () => {
+ getServiceWidget.mockResolvedValue({ type: "mapped", url: "http://example" });
+
+ const req = createReq({
+ method: "GET",
+ query: { group: "g", service: "svc", endpoint: "Pause", index: "0" },
+ });
+ const res = createMockRes();
+
+ await handler(req, res);
+
+ expect(mappedHandler).not.toHaveBeenCalled();
+ expect(res.statusCode).toBe(403);
+ expect(res.body).toEqual({ error: "Unsupported method" });
+ });
+ });
+
+ describe("dispatch guards", () => {
+ it("rejects an endpoint that fails the allowedEndpoints test", async () => {
+ getServiceWidget.mockResolvedValue({ type: "allowed", url: "http://example" });
+
+ const req = createReq({ query: { group: "g", service: "svc", endpoint: "shutdown", index: "0" } });
+ const res = createMockRes();
+
+ await handler(req, res);
+
+ expect(allowedHandler).not.toHaveBeenCalled();
+ expect(res.statusCode).toBe(403);
+ expect(res.body).toEqual({ error: "Unmapped proxy request." });
+ });
+
+ it("rejects an unknown widget type", async () => {
+ getServiceWidget.mockResolvedValue({ type: "nope", url: "http://example" });
+
+ const req = createReq({ query: { group: "g", service: "svc", index: "0" } });
+ const res = createMockRes();
+
+ await handler(req, res);
+
+ expect(res.statusCode).toBe(403);
+ expect(res.body).toEqual({ error: "Unknown proxy service type" });
+ });
+ });
+});
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
index d5c5296cf..31b095bf1 100644
--- a/src/pages/index.jsx
+++ b/src/pages/index.jsx
@@ -1,4 +1,14 @@
import classNames from "classnames";
+import { useTranslation } from "next-i18next/pages";
+import { serverSideTranslations } from "next-i18next/pages/serverSideTranslations";
+import dynamic from "next/dynamic";
+import Head from "next/head";
+import { useRouter } from "next/router";
+import Script from "next/script";
+import { useCallback, useContext, useEffect, useMemo, useState } from "react";
+import { BiError } from "react-icons/bi";
+import useSWR, { SWRConfig } from "swr";
+
import BookmarksGroup from "components/bookmarks/group";
import ErrorBoundary from "components/errorboundry";
import QuickLaunch from "components/quicklaunch";
@@ -6,22 +16,12 @@ import ServicesGroup from "components/services/group";
import Tab, { slugifyAndEncode } from "components/tab";
import Revalidate from "components/toggles/revalidate";
import Widget from "components/widgets/widget";
-import { useTranslation } from "next-i18next/pages";
-import { serverSideTranslations } from "next-i18next/pages/serverSideTranslations";
-import dynamic from "next/dynamic";
-import Head from "next/head";
-import { useRouter } from "next/router";
-import Script from "next/script";
-import { useContext, useEffect, useMemo, useState } from "react";
-import { BiError } from "react-icons/bi";
-import useSWR, { SWRConfig } from "swr";
+import { bookmarksResponse, servicesResponse, widgetsResponse } from "utils/config/api-response";
+import { getSettings } from "utils/config/config";
import { ColorContext } from "utils/contexts/color";
import { SettingsContext } from "utils/contexts/settings";
import { TabContext } from "utils/contexts/tab";
import { ThemeContext } from "utils/contexts/theme";
-
-import { bookmarksResponse, servicesResponse, widgetsResponse } from "utils/config/api-response";
-import { getSettings } from "utils/config/config";
import useWindowFocus from "utils/hooks/window-focus";
import createLogger from "utils/logger";
import themes from "utils/styles/themes";
@@ -102,7 +102,29 @@ function Index({ initialSettings, fallback }) {
const [stale, setStale] = useState(false);
const { data: errorsData } = useSWR("/api/validate");
const { error: validateError } = errorsData || {};
- const { data: hashData, mutate: mutateHash } = useSWR("/api/hash");
+
+ const handleHashData = useCallback((hashData) => {
+ if (typeof window === "undefined" || !hashData?.hash) return;
+
+ const previousHash = localStorage.getItem("hash");
+
+ if (!previousHash) {
+ localStorage.setItem("hash", hashData.hash);
+ }
+
+ if (previousHash && previousHash !== hashData.hash) {
+ setStale(true);
+ localStorage.setItem("hash", hashData.hash);
+
+ fetch("/api/revalidate").then((res) => {
+ if (res.ok) {
+ window.location.reload();
+ }
+ });
+ }
+ }, []);
+
+ const { mutate: mutateHash } = useSWR("/api/hash", { onSuccess: handleHashData });
useEffect(() => {
if (windowFocused) {
@@ -110,29 +132,6 @@ function Index({ initialSettings, fallback }) {
}
}, [windowFocused, mutateHash]);
- useEffect(() => {
- if (hashData) {
- if (typeof window !== "undefined") {
- const previousHash = localStorage.getItem("hash");
-
- if (!previousHash) {
- localStorage.setItem("hash", hashData.hash);
- }
-
- if (previousHash && previousHash !== hashData.hash) {
- setStale(true);
- localStorage.setItem("hash", hashData.hash);
-
- fetch("/api/revalidate").then((res) => {
- if (res.ok) {
- window.location.reload();
- }
- });
- }
- }
- }
- }, [hashData]);
-
if (validateError) {
return (
@@ -264,6 +263,13 @@ function Home({ initialSettings }) {
e.key.match(/([à-ü]|[À-Ü]|!)/g) ||
(e.key === "v" && (e.ctrlKey || e.metaKey))
) {
+ if (e.key.length === 1 && !(e.key === "v" && (e.ctrlKey || e.metaKey))) {
+ e.preventDefault();
+ // whitespace opens the search but shouldn't be in it
+ if (e.key.trim()) {
+ setSearchString((currentSearchString) => currentSearchString + e.key);
+ }
+ }
setSearching(true);
} else if (e.key === "Escape") {
setSearchString("");
@@ -420,10 +426,11 @@ function Home({ initialSettings }) {
/>
{settings.disableIndexing &&
}
{settings.base &&
}
- {settings.favicon ? (
+ {/* from props, not context so its set before running JS */}
+ {initialSettings.favicon ? (
<>
-
-
+
+
>
) : (
<>
diff --git a/src/test-utils/render-with-providers.jsx b/src/test-utils/render-with-providers.jsx
index 2f9768912..7d6073fa9 100644
--- a/src/test-utils/render-with-providers.jsx
+++ b/src/test-utils/render-with-providers.jsx
@@ -1,4 +1,5 @@
import { render } from "@testing-library/react";
+
import { SettingsContext } from "utils/contexts/settings";
export function renderWithProviders(ui, { settings = {} } = {}) {
diff --git a/src/utils/contexts/color.jsx b/src/utils/contexts/color.jsx
index f75ced94f..d779a58b6 100644
--- a/src/utils/contexts/color.jsx
+++ b/src/utils/contexts/color.jsx
@@ -30,10 +30,6 @@ export function ColorProvider({ initialTheme, children }) {
lastColor = rawColor;
};
- useEffect(() => {
- if (initialTheme !== undefined) setColor(initialTheme ?? getInitialColor());
- }, [initialTheme]);
-
useEffect(() => {
rawSetColor(color);
}, [color]);
diff --git a/src/utils/contexts/settings.jsx b/src/utils/contexts/settings.jsx
index 00699b41a..1837ec275 100644
--- a/src/utils/contexts/settings.jsx
+++ b/src/utils/contexts/settings.jsx
@@ -1,14 +1,10 @@
-import { createContext, useEffect, useMemo, useState } from "react";
+import { createContext, useMemo, useState } from "react";
export const SettingsContext = createContext();
export function SettingsProvider({ initialSettings, children }) {
const [settings, setSettings] = useState(() => initialSettings ?? {});
- useEffect(() => {
- if (initialSettings !== undefined) setSettings(initialSettings ?? {});
- }, [initialSettings]);
-
const value = useMemo(() => ({ settings, setSettings }), [settings]);
return
{children};
diff --git a/src/utils/contexts/tab.jsx b/src/utils/contexts/tab.jsx
index c18fc2c94..316f43d72 100644
--- a/src/utils/contexts/tab.jsx
+++ b/src/utils/contexts/tab.jsx
@@ -1,14 +1,10 @@
-import { createContext, useEffect, useMemo, useState } from "react";
+import { createContext, useMemo, useState } from "react";
export const TabContext = createContext();
export function TabProvider({ initialTab, children }) {
const [activeTab, setActiveTab] = useState(() => initialTab ?? false);
- useEffect(() => {
- if (initialTab !== undefined) setActiveTab(initialTab ?? false);
- }, [initialTab]);
-
const value = useMemo(() => ({ activeTab, setActiveTab }), [activeTab]);
return
{children};
diff --git a/src/utils/contexts/theme.jsx b/src/utils/contexts/theme.jsx
index 81acff46a..3e4436ab5 100644
--- a/src/utils/contexts/theme.jsx
+++ b/src/utils/contexts/theme.jsx
@@ -31,10 +31,6 @@ export function ThemeProvider({ initialTheme, children }) {
localStorage.setItem("theme-mode", rawTheme);
};
- useEffect(() => {
- if (initialTheme !== undefined) setTheme(initialTheme ?? getInitialTheme());
- }, [initialTheme]);
-
useEffect(() => {
rawSetTheme(theme);
}, [theme]);
diff --git a/src/utils/hooks/use-current-time.js b/src/utils/hooks/use-current-time.js
new file mode 100644
index 000000000..a5640558d
--- /dev/null
+++ b/src/utils/hooks/use-current-time.js
@@ -0,0 +1,45 @@
+import { useSyncExternalStore } from "react";
+
+const MINUTE = 60_000;
+const stores = new Map();
+
+function getStore(refreshInterval) {
+ const existing = stores.get(refreshInterval);
+ if (existing) return existing;
+
+ const listeners = new Set();
+ let now = null; // null until mounted, so SSR and hydration render the same thing
+ let timer = null;
+
+ const store = {
+ getSnapshot: () => now,
+ subscribe: (onStoreChange) => {
+ listeners.add(onStoreChange);
+
+ if (!timer) {
+ now = Date.now();
+ timer = setInterval(() => {
+ now = Date.now();
+ listeners.forEach((listener) => listener());
+ }, refreshInterval);
+ }
+
+ return () => {
+ listeners.delete(onStoreChange);
+ if (!listeners.size) {
+ clearInterval(timer);
+ timer = null;
+ }
+ };
+ },
+ };
+
+ stores.set(refreshInterval, store);
+ return store;
+}
+
+// Returns null on the server and during hydration, then the current time.
+export default function useCurrentTime(refreshInterval = MINUTE) {
+ const { subscribe, getSnapshot } = getStore(refreshInterval);
+ return useSyncExternalStore(subscribe, getSnapshot, () => null);
+}
diff --git a/src/utils/hooks/use-current-time.test.jsx b/src/utils/hooks/use-current-time.test.jsx
new file mode 100644
index 000000000..ae8b02d34
--- /dev/null
+++ b/src/utils/hooks/use-current-time.test.jsx
@@ -0,0 +1,32 @@
+// @vitest-environment jsdom
+
+import { act, render, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+
+import useCurrentTime from "./use-current-time";
+
+function CurrentTime({ refreshInterval }) {
+ return
{useCurrentTime(refreshInterval)};
+}
+
+describe("utils/hooks/use-current-time", () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ vi.setSystemTime(1_000);
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it("updates at the requested interval", () => {
+ render(
);
+ expect(screen.getByText("1000")).toBeInTheDocument();
+
+ act(() => {
+ vi.advanceTimersByTime(5_000);
+ });
+
+ expect(screen.getByText("6000")).toBeInTheDocument();
+ });
+});
diff --git a/src/utils/hooks/window-focus.js b/src/utils/hooks/window-focus.js
index a221e48ea..0cf5585e4 100644
--- a/src/utils/hooks/window-focus.js
+++ b/src/utils/hooks/window-focus.js
@@ -1,26 +1,17 @@
-import { useEffect, useState } from "react";
+import { useSyncExternalStore } from "react";
const hasFocus = () => typeof document !== "undefined" && document.hasFocus();
-const useWindowFocus = () => {
- const [focused, setFocused] = useState(hasFocus);
+const subscribe = (onFocusChange) => {
+ window.addEventListener("focus", onFocusChange);
+ window.addEventListener("blur", onFocusChange);
- useEffect(() => {
- setFocused(hasFocus());
-
- const onFocus = () => setFocused(true);
- const onBlur = () => setFocused(false);
-
- window.addEventListener("focus", onFocus);
- window.addEventListener("blur", onBlur);
-
- return () => {
- window.removeEventListener("focus", onFocus);
- window.removeEventListener("blur", onBlur);
- };
- }, []);
-
- return focused;
+ return () => {
+ window.removeEventListener("focus", onFocusChange);
+ window.removeEventListener("blur", onFocusChange);
+ };
};
+const useWindowFocus = () => useSyncExternalStore(subscribe, hasFocus, () => false);
+
export default useWindowFocus;
diff --git a/src/utils/hooks/window-focus.test.jsx b/src/utils/hooks/window-focus.test.jsx
index 818fb15fe..2491c39e9 100644
--- a/src/utils/hooks/window-focus.test.jsx
+++ b/src/utils/hooks/window-focus.test.jsx
@@ -12,15 +12,17 @@ function Fixture() {
describe("utils/hooks/window-focus", () => {
it("tracks focus/blur events", async () => {
- vi.spyOn(document, "hasFocus").mockReturnValue(true);
+ const hasFocus = vi.spyOn(document, "hasFocus").mockReturnValue(true);
render(
);
expect(screen.getByTestId("focused")).toHaveTextContent("true");
+ hasFocus.mockReturnValue(false);
window.dispatchEvent(new Event("blur"));
await waitFor(() => expect(screen.getByTestId("focused")).toHaveTextContent("false"));
+ hasFocus.mockReturnValue(true);
window.dispatchEvent(new Event("focus"));
await waitFor(() => expect(screen.getByTestId("focused")).toHaveTextContent("true"));
});
diff --git a/src/utils/proxy/use-widget-api.js b/src/utils/proxy/use-widget-api.js
index 5505a87e7..966c9e525 100644
--- a/src/utils/proxy/use-widget-api.js
+++ b/src/utils/proxy/use-widget-api.js
@@ -2,13 +2,13 @@ import useSWR from "swr";
import { formatProxyUrl } from "./api-helpers";
-export default function useWidgetAPI(widget, ...options) {
- const config = {};
- if (options && options[1]?.refreshInterval) {
- config.refreshInterval = options[1].refreshInterval;
+export default function useWidgetAPI(widget, endpoint, queryParams, swrConfig = {}) {
+ const config = { ...swrConfig };
+ if (queryParams?.refreshInterval) {
+ config.refreshInterval = queryParams.refreshInterval;
}
- let url = formatProxyUrl(widget, ...options);
- if (options[0] === "") {
+ let url = formatProxyUrl(widget, endpoint, queryParams);
+ if (endpoint === "") {
url = null;
}
const { data, error, mutate } = useSWR(url, config);
diff --git a/src/utils/proxy/use-widget-api.test.js b/src/utils/proxy/use-widget-api.test.js
index eb7e7d1b4..febdf9908 100644
--- a/src/utils/proxy/use-widget-api.test.js
+++ b/src/utils/proxy/use-widget-api.test.js
@@ -28,6 +28,18 @@ describe("utils/proxy/use-widget-api", () => {
expect(result.mutate).toBe("m");
});
+ it("passes additional SWR configuration separately from the proxy query", () => {
+ useSWR.mockReturnValue({ data: undefined, error: undefined, mutate: vi.fn() });
+
+ const widget = { service_group: "g", service_name: "s", index: 0 };
+ const onSuccess = vi.fn();
+ useWidgetAPI(widget, "status", { refreshInterval: 123 }, { onSuccess });
+
+ const [url, config] = useSWR.mock.calls[0];
+ expect(url).not.toContain("onSuccess");
+ expect(config).toEqual({ refreshInterval: 123, onSuccess });
+ });
+
it("returns data.error as the top-level error", () => {
const dataError = { message: "nope" };
useSWR.mockReturnValue({ data: { error: dataError }, error: undefined, mutate: vi.fn() });
diff --git a/src/utils/widget-fields.js b/src/utils/widget-fields.js
new file mode 100644
index 000000000..658d72cca
--- /dev/null
+++ b/src/utils/widget-fields.js
@@ -0,0 +1,12 @@
+export default function withWidgetFields(service, defaultFields, maxFields = 4) {
+ const configuredFields = service.widget.fields;
+ const fields = (configuredFields?.length ? configuredFields : defaultFields).slice(0, maxFields);
+
+ return {
+ ...service,
+ widget: {
+ ...service.widget,
+ fields,
+ },
+ };
+}
diff --git a/src/utils/widget-fields.test.js b/src/utils/widget-fields.test.js
new file mode 100644
index 000000000..0cddb1142
--- /dev/null
+++ b/src/utils/widget-fields.test.js
@@ -0,0 +1,35 @@
+import { describe, expect, it } from "vitest";
+
+import withWidgetFields from "./widget-fields";
+
+describe("utils/widget-fields", () => {
+ it("applies defaults without modifying the service", () => {
+ const service = { name: "Example", widget: { type: "example" } };
+
+ const normalizedService = withWidgetFields(service, ["one", "two"]);
+
+ expect(normalizedService).toEqual({
+ name: "Example",
+ widget: { type: "example", fields: ["one", "two"] },
+ });
+ expect(service).toEqual({ name: "Example", widget: { type: "example" } });
+ });
+
+ it("treats an explicitly empty fields list as unset", () => {
+ const service = { widget: { type: "example", fields: [] } };
+
+ const normalizedService = withWidgetFields(service, ["one", "two"]);
+
+ expect(normalizedService.widget.fields).toEqual(["one", "two"]);
+ });
+
+ it("copies and limits configured fields", () => {
+ const service = { widget: { type: "example", fields: ["one", "two", "three"] } };
+
+ const normalizedService = withWidgetFields(service, ["default"], 2);
+
+ expect(normalizedService.widget.fields).toEqual(["one", "two"]);
+ expect(normalizedService.widget.fields).not.toBe(service.widget.fields);
+ expect(service.widget.fields).toEqual(["one", "two", "three"]);
+ });
+});
diff --git a/src/widgets/adguard/component.jsx b/src/widgets/adguard/component.jsx
index dd61e5daf..7b2099cac 100644
--- a/src/widgets/adguard/component.jsx
+++ b/src/widgets/adguard/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/apcups/component.jsx b/src/widgets/apcups/component.jsx
index 85e621dbb..1eb90b81e 100644
--- a/src/widgets/apcups/component.jsx
+++ b/src/widgets/apcups/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/arcane/component.jsx b/src/widgets/arcane/component.jsx
index 20307bfdc..5e8ed3ff4 100644
--- a/src/widgets/arcane/component.jsx
+++ b/src/widgets/arcane/component.jsx
@@ -1,21 +1,17 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-const MAX_FIELDS = 4;
+const DEFAULT_FIELDS = ["running", "stopped", "total", "image_updates"];
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
- if (!widget.fields) {
- widget.fields = ["running", "stopped", "total", "image_updates"];
- } else if (widget.fields.length > MAX_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_FIELDS);
- }
-
const envNotSet = widget.env == null || widget.env === "";
const { data: containers, error: containersError } = useWidgetAPI(widget, envNotSet ? "" : "containers");
diff --git a/src/widgets/arcane/component.test.jsx b/src/widgets/arcane/component.test.jsx
index 466cbcc71..3750c4520 100644
--- a/src/widgets/arcane/component.test.jsx
+++ b/src/widgets/arcane/component.test.jsx
@@ -57,7 +57,7 @@ describe("widgets/arcane/component", () => {
expect(screen.getByText("arcane.image_updates")).toBeInTheDocument();
});
- it("truncates custom fields to the max allowed", () => {
+ it("renders only the first four custom fields without mutating the input", () => {
useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined }));
const service = {
@@ -65,7 +65,8 @@ describe("widgets/arcane/component", () => {
};
const { container } = renderWithProviders(
, { settings: { hideErrors: false } });
- // sliced to first four entries
+ // The helper caps the copied fields used for rendering and leaves the configured service unchanged.
+ expect(service.widget.fields).toEqual(["running", "stopped", "total", "images", "images_unused"]);
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("docker.running")).toBeInTheDocument();
expect(screen.getByText("dockhand.stopped")).toBeInTheDocument();
diff --git a/src/widgets/argocd/component.jsx b/src/widgets/argocd/component.jsx
index f61bed432..cc4a1b0ce 100644
--- a/src/widgets/argocd/component.jsx
+++ b/src/widgets/argocd/component.jsx
@@ -1,20 +1,14 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const DEFAULT_FIELDS = ["apps", "synced", "outOfSync", "healthy"];
+
+export default function Component({ service: configuredService }) {
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
- if (!widget.fields) {
- widget.fields = ["apps", "synced", "outOfSync", "healthy"];
- }
-
- const MAX_ALLOWED_FIELDS = 4;
- if (widget.fields.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
const { data: appsData, error: appsError } = useWidgetAPI(widget, "applications");
const appCounts = widget.fields.map((status) => {
diff --git a/src/widgets/argocd/component.test.jsx b/src/widgets/argocd/component.test.jsx
index 88b102643..ed4b29116 100644
--- a/src/widgets/argocd/component.test.jsx
+++ b/src/widgets/argocd/component.test.jsx
@@ -25,7 +25,7 @@ describe("widgets/argocd/component", () => {
const service = { widget: { type: "argocd", fields: ["apps", "synced", "outOfSync", "healthy", "extra"] } };
const { container } = renderWithProviders(
, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["apps", "synced", "outOfSync", "healthy"]);
+ expect(service.widget.fields).toEqual(["apps", "synced", "outOfSync", "healthy", "extra"]);
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("argocd.apps")).toBeInTheDocument();
expect(screen.getByText("argocd.synced")).toBeInTheDocument();
diff --git a/src/widgets/atsumeru/component.jsx b/src/widgets/atsumeru/component.jsx
index 33952a189..6a325a136 100644
--- a/src/widgets/atsumeru/component.jsx
+++ b/src/widgets/atsumeru/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/audiobookshelf/component.jsx b/src/widgets/audiobookshelf/component.jsx
index d24dfb3c9..3d94f8f04 100755
--- a/src/widgets/audiobookshelf/component.jsx
+++ b/src/widgets/audiobookshelf/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/authentik/component.jsx b/src/widgets/authentik/component.jsx
index dcb2fe6e4..a233ba829 100644
--- a/src/widgets/authentik/component.jsx
+++ b/src/widgets/authentik/component.jsx
@@ -1,11 +1,15 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+import useCurrentTime from "utils/hooks/use-current-time";
import useWidgetAPI from "utils/proxy/use-widget-api";
+const DAY = 24 * 60 * 60 * 1000;
+
export default function Component({ service }) {
const { t } = useTranslation();
+ const currentTime = useCurrentTime();
const { widget } = service;
const isV2 = widget.version === 2;
@@ -45,7 +49,7 @@ export default function Component({ service }) {
switch (widget.version) {
// v1 is default
default:
- const yesterday = new Date(Date.now()).setHours(-24);
+ const yesterday = currentTime - DAY;
loginsLast24H = loginsData.reduce(
(total, current) => (current.x_cord >= yesterday ? total + current.y_cord : total),
0,
diff --git a/src/widgets/authentik/component.test.jsx b/src/widgets/authentik/component.test.jsx
index e3b39d0d8..d49f5c774 100644
--- a/src/widgets/authentik/component.test.jsx
+++ b/src/widgets/authentik/component.test.jsx
@@ -71,7 +71,8 @@ describe("widgets/authentik/component", () => {
it("computes v1 login/failed counts for entries within the last 24h window", () => {
vi.useFakeTimers();
- vi.setSystemTime(new Date("2026-01-02T00:00:00Z"));
+ // Use midday so a mistaken previous-midnight cutoff would include the 25-hour-old entries.
+ vi.setSystemTime(new Date("2026-01-02T12:00:00Z"));
const now = Date.now();
const oneHourAgo = now - 60 * 60 * 1000;
diff --git a/src/widgets/autobrr/component.jsx b/src/widgets/autobrr/component.jsx
index 5164cc309..7b652b610 100644
--- a/src/widgets/autobrr/component.jsx
+++ b/src/widgets/autobrr/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/azuredevops/component.jsx b/src/widgets/azuredevops/component.jsx
index c963e3d1e..a5cf95b1c 100644
--- a/src/widgets/azuredevops/component.jsx
+++ b/src/widgets/azuredevops/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/backrest/component.jsx b/src/widgets/backrest/component.jsx
index ab52efa05..ff889ec5b 100644
--- a/src/widgets/backrest/component.jsx
+++ b/src/widgets/backrest/component.jsx
@@ -1,15 +1,16 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
const BACKREST_DEFAULT_FIELDS = ["num_success_latest", "num_failure_latest", "num_failure_30", "bytes_added_30"];
-const MAX_ALLOWED_FIELDS = 4;
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, BACKREST_DEFAULT_FIELDS);
const { widget } = service;
const { data, error } = useWidgetAPI(widget, "summary");
@@ -18,12 +19,6 @@ export default function Component({ service }) {
return
;
}
- if (!widget.fields?.length) {
- widget.fields = BACKREST_DEFAULT_FIELDS;
- } else if (widget.fields.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
if (!data) {
return (
diff --git a/src/widgets/backrest/component.test.jsx b/src/widgets/backrest/component.test.jsx
index 85616c601..20d93e863 100644
--- a/src/widgets/backrest/component.test.jsx
+++ b/src/widgets/backrest/component.test.jsx
@@ -24,12 +24,7 @@ describe("widgets/backrest/component", () => {
const service = { widget: { type: "backrest" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual([
- "num_success_latest",
- "num_failure_latest",
- "num_failure_30",
- "bytes_added_30",
- ]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("backrest.num_success_latest")).toBeInTheDocument();
@@ -48,7 +43,7 @@ describe("widgets/backrest/component", () => {
renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["a", "b", "c", "d"]);
+ expect(service.widget.fields).toEqual(["a", "b", "c", "d", "e"]);
});
it("renders values and respects field filtering", () => {
diff --git a/src/widgets/bazarr/component.jsx b/src/widgets/bazarr/component.jsx
index 0e80180fe..ce443c318 100644
--- a/src/widgets/bazarr/component.jsx
+++ b/src/widgets/bazarr/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/beszel/component.jsx b/src/widgets/beszel/component.jsx
index de3f52aa7..4eae0c3f6 100644
--- a/src/widgets/beszel/component.jsx
+++ b/src/widgets/beszel/component.jsx
@@ -1,25 +1,23 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const SUMMARY_FIELDS = ["systems", "up"];
+const SYSTEM_FIELDS = ["name", "status", "cpu", "memory"];
+
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const defaultFields = configuredService.widget.systemId ? SYSTEM_FIELDS : SUMMARY_FIELDS;
+ const service = withWidgetFields(configuredService, defaultFields);
const { widget } = service;
const { systemId } = widget;
const { data: systems, error: systemsError } = useWidgetAPI(widget, "systems");
- const MAX_ALLOWED_FIELDS = 4;
- if (!widget.fields?.length > 0) {
- widget.fields = systemId ? ["name", "status", "cpu", "memory"] : ["systems", "up"];
- }
- if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
let system = null;
let finalError = systemsError;
diff --git a/src/widgets/beszel/component.test.jsx b/src/widgets/beszel/component.test.jsx
index ba3d0c618..454d85d11 100644
--- a/src/widgets/beszel/component.test.jsx
+++ b/src/widgets/beszel/component.test.jsx
@@ -23,7 +23,7 @@ describe("widgets/beszel/component", () => {
const service = { widget: { type: "beszel" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["systems", "up"]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(2);
expect(screen.getByText("beszel.systems")).toBeInTheDocument();
expect(screen.getByText("beszel.up")).toBeInTheDocument();
@@ -66,7 +66,7 @@ describe("widgets/beszel/component", () => {
const service = { widget: { type: "beszel", systemId: "sys1" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["name", "status", "cpu", "memory"]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expectBlockValue(container, "beszel.name", "MySystem");
diff --git a/src/widgets/booklore/component.jsx b/src/widgets/booklore/component.jsx
index 99d8567f0..69e5bfa45 100644
--- a/src/widgets/booklore/component.jsx
+++ b/src/widgets/booklore/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/caddy/component.jsx b/src/widgets/caddy/component.jsx
index cfe36f61c..0d43e5054 100644
--- a/src/widgets/caddy/component.jsx
+++ b/src/widgets/caddy/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/calendar/component.jsx b/src/widgets/calendar/component.jsx
index 7c4b7c709..b73263a04 100644
--- a/src/widgets/calendar/component.jsx
+++ b/src/widgets/calendar/component.jsx
@@ -1,13 +1,14 @@
-import Container from "components/services/widget/container";
import { DateTime } from "luxon";
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
import { useContext, useEffect, useMemo, useState } from "react";
-import { SettingsContext } from "utils/contexts/settings";
import Agenda from "./agenda";
import Monthly from "./monthly";
+import Container from "components/services/widget/container";
+import { SettingsContext } from "utils/contexts/settings";
+
const colorVariants = {
// https://tailwindcss.com/docs/content-configuration#dynamic-class-names
amber: "bg-amber-500",
@@ -38,16 +39,16 @@ const colorVariants = {
export default function Component({ service }) {
const { widget } = service;
const { i18n } = useTranslation();
- const [showDate, setShowDate] = useState(null);
const [events, setEvents] = useState({});
const nowDate = DateTime.now().setLocale(i18n.language);
const currentDate = widget?.timezone ? nowDate.setZone(widget?.timezone).startOf("day") : nowDate;
+ const [showDate, setShowDate] = useState(null);
const { settings } = useContext(SettingsContext);
useEffect(() => {
- if (!showDate) {
- setShowDate(currentDate);
- }
+ // seeded after mount, not during render: "today" is client-only and would break hydration
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ if (!showDate) setShowDate(currentDate);
}, [showDate, currentDate]);
// params for API fetch
diff --git a/src/widgets/calendar/integrations/ical.jsx b/src/widgets/calendar/integrations/ical.jsx
index bad4a6ac9..1c20dee5d 100644
--- a/src/widgets/calendar/integrations/ical.jsx
+++ b/src/widgets/calendar/integrations/ical.jsx
@@ -1,7 +1,7 @@
import ICAL from "ical.js";
import { DateTime } from "luxon";
import { useTranslation } from "next-i18next/pages";
-import { useEffect } from "react";
+import { useEffect, useMemo } from "react";
import Error from "../../../components/services/widget/error";
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
@@ -17,65 +17,67 @@ function simpleHash(str) {
return Math.abs(hash).toString(36);
}
+function buildEvent(event, type) {
+ return {
+ id: event.getFirstPropertyValue("uid"),
+ type,
+ title: event.getFirstPropertyValue("summary"),
+ rrule: event.getFirstPropertyValue("rrule"),
+ dtstart:
+ event.getFirstPropertyValue("dtstart") ||
+ event.getFirstPropertyValue("due") ||
+ event.getFirstPropertyValue("completed") ||
+ ICAL.Time.now(), // handles events without a date
+ dtend:
+ event.getFirstPropertyValue("dtend") ||
+ event.getFirstPropertyValue("due") ||
+ event.getFirstPropertyValue("completed") ||
+ ICAL.Time.now(), // handles events without a date
+ location: event.getFirstPropertyValue("location"),
+ status: event.getFirstPropertyValue("status"),
+ url: event.getFirstPropertyValue("url"),
+ };
+}
+
export default function Integration({ config, params, setEvents, hideErrors, timezone }) {
const { t } = useTranslation();
const { data: icalData, error: icalError } = useWidgetAPI(config, config.name, {
refreshInterval: 300000, // 5 minutes
});
+ const { events, dataError } = useMemo(() => {
+ if (icalError || !icalData || icalData.error) {
+ return { events: [], dataError: undefined };
+ }
+
+ if (!icalData.data) {
+ return {
+ events: [],
+ dataError: { message: `'${config.name}': ${t("calendar.errorWhenLoadingData")}` },
+ };
+ }
+
+ const jCal = ICAL.parse(icalData.data);
+ const vCalendar = new ICAL.Component(jCal);
+ const parsedEvents = [
+ ...vCalendar.getAllSubcomponents("vevent").map((event) => buildEvent(event, "vevent")),
+ ...vCalendar.getAllSubcomponents("vtodo").map((todo) => buildEvent(todo, "vtodo")),
+ ];
+
+ return {
+ events: parsedEvents,
+ dataError:
+ parsedEvents.length === 0 ? { message: `'${config.name}': ${t("calendar.noEventsFound")}` } : undefined,
+ };
+ }, [icalData, icalError, config.name, t]);
+
useEffect(() => {
const { showName = false } = config?.params || {};
- let events = [];
-
- if (!icalError && icalData && !icalData.error) {
- if (!icalData.data) {
- icalData.error = { message: `'${config.name}': ${t("calendar.errorWhenLoadingData")}` };
- return;
- }
-
- const jCal = ICAL.parse(icalData.data);
- const vCalendar = new ICAL.Component(jCal);
-
- const buildEvent = (event, type) => {
- return {
- id: event.getFirstPropertyValue("uid"),
- type,
- title: event.getFirstPropertyValue("summary"),
- rrule: event.getFirstPropertyValue("rrule"),
- dtstart:
- event.getFirstPropertyValue("dtstart") ||
- event.getFirstPropertyValue("due") ||
- event.getFirstPropertyValue("completed") ||
- ICAL.Time.now(), // handles events without a date
- dtend:
- event.getFirstPropertyValue("dtend") ||
- event.getFirstPropertyValue("due") ||
- event.getFirstPropertyValue("completed") ||
- ICAL.Time.now(), // handles events without a date
- location: event.getFirstPropertyValue("location"),
- status: event.getFirstPropertyValue("status"),
- url: event.getFirstPropertyValue("url"),
- };
- };
-
- const getEvents = () => {
- const vEvents = vCalendar.getAllSubcomponents("vevent").map((event) => buildEvent(event, "vevent"));
-
- const vTodos = vCalendar.getAllSubcomponents("vtodo").map((todo) => buildEvent(todo, "vtodo"));
-
- return [...vEvents, ...vTodos];
- };
-
- events = getEvents();
- if (events.length === 0) {
- icalData.error = { message: `'${config.name}': ${t("calendar.noEventsFound")}` };
- }
- }
const startDate = DateTime.fromISO(params.start);
const endDate = DateTime.fromISO(params.end);
- if (icalError || events.length === 0 || !startDate.isValid || !endDate.isValid) {
+ if (events.length === 0 || !startDate.isValid || !endDate.isValid) {
return;
}
@@ -113,7 +115,7 @@ export default function Integration({ config, params, setEvents, hideErrors, tim
return occurrences;
};
- const eventsToAdd = [];
+ const eventsToAdd = {};
events.forEach((event) => {
const occurrences = getOcurrencesFromRange(event);
@@ -154,8 +156,8 @@ export default function Integration({ config, params, setEvents, hideErrors, tim
});
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
- }, [icalData, icalError, config, params, setEvents, timezone, t]);
+ }, [events, config, params, setEvents, timezone]);
- const error = icalError ?? icalData?.error;
+ const error = icalError ?? icalData?.error ?? dataError;
return error && !hideErrors && ;
}
diff --git a/src/widgets/calendar/integrations/ical.test.jsx b/src/widgets/calendar/integrations/ical.test.jsx
index bd0b4f873..d1d0fb019 100644
--- a/src/widgets/calendar/integrations/ical.test.jsx
+++ b/src/widgets/calendar/integrations/ical.test.jsx
@@ -1,6 +1,6 @@
// @vitest-environment jsdom
-import { render, waitFor } from "@testing-library/react";
+import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
const { useWidgetAPI } = vi.hoisted(() => ({
@@ -12,6 +12,45 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
import Integration from "./ical";
describe("widgets/calendar/integrations/ical", () => {
+ it("reports a missing calendar payload without mutating the response", () => {
+ const data = {};
+ useWidgetAPI.mockReturnValue({ data, error: undefined });
+
+ render(
+ ,
+ );
+
+ expect(screen.getByText(/'Work': calendar\.errorWhenLoadingData/)).toBeInTheDocument();
+ expect(data).toEqual({});
+ });
+
+ it("reports a calendar with no events without mutating the response", () => {
+ const data = {
+ data: ["BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Test//EN", "END:VCALENDAR", ""].join("\n"),
+ };
+ const originalData = structuredClone(data);
+ useWidgetAPI.mockReturnValue({ data, error: undefined });
+
+ render(
+ ,
+ );
+
+ expect(screen.getByText(/'Empty': calendar\.noEventsFound/)).toBeInTheDocument();
+ expect(data).toEqual(originalData);
+ });
+
it("adds parsed events within the date range", async () => {
useWidgetAPI.mockReturnValue({
data: {
diff --git a/src/widgets/calibreweb/component.jsx b/src/widgets/calibreweb/component.jsx
index 993ada471..c6eef7947 100644
--- a/src/widgets/calibreweb/component.jsx
+++ b/src/widgets/calibreweb/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/changedetectionio/component.jsx b/src/widgets/changedetectionio/component.jsx
index 60da39a05..b63716b8b 100644
--- a/src/widgets/changedetectionio/component.jsx
+++ b/src/widgets/changedetectionio/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/channelsdvrserver/component.jsx b/src/widgets/channelsdvrserver/component.jsx
index b05c46146..acf6c27d1 100644
--- a/src/widgets/channelsdvrserver/component.jsx
+++ b/src/widgets/channelsdvrserver/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/checkmk/component.jsx b/src/widgets/checkmk/component.jsx
index 4a44d3e77..083eb299f 100644
--- a/src/widgets/checkmk/component.jsx
+++ b/src/widgets/checkmk/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/cloudflared/component.jsx b/src/widgets/cloudflared/component.jsx
index 790a5f34b..abae61bb0 100644
--- a/src/widgets/cloudflared/component.jsx
+++ b/src/widgets/cloudflared/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/coinmarketcap/component.jsx b/src/widgets/coinmarketcap/component.jsx
index c2a15b261..17d98d12f 100644
--- a/src/widgets/coinmarketcap/component.jsx
+++ b/src/widgets/coinmarketcap/component.jsx
@@ -1,10 +1,10 @@
import classNames from "classnames";
-import Dropdown from "components/services/dropdown";
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import { useState } from "react";
+import Dropdown from "components/services/dropdown";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/crowdsec/component.jsx b/src/widgets/crowdsec/component.jsx
index 7eea1fc9c..7f84f8a93 100644
--- a/src/widgets/crowdsec/component.jsx
+++ b/src/widgets/crowdsec/component.jsx
@@ -1,17 +1,22 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+const ALERTS_LIMIT = 500;
+
export default function Component({ service }) {
const { t } = useTranslation();
const { widget } = service;
- const { data: alerts, error: alertsError } = useWidgetAPI(widget, !!widget.limit24h ? "alerts24h" : "alerts");
+ const { data: alerts, error: alertsError } = useWidgetAPI(widget, "alerts");
const { data: bans, error: bansError } = useWidgetAPI(widget, "bans");
+ const alertsCount = alerts?.length ?? 0;
+ const alertsValue = t("common.number", { value: alertsCount });
+
if (alertsError || bansError) {
return ;
}
@@ -27,7 +32,7 @@ export default function Component({ service }) {
return (
-
+ = ALERTS_LIMIT ? `${alertsValue}+` : alertsValue} />
);
diff --git a/src/widgets/crowdsec/component.test.jsx b/src/widgets/crowdsec/component.test.jsx
index abc4c8696..9802eaed4 100644
--- a/src/widgets/crowdsec/component.test.jsx
+++ b/src/widgets/crowdsec/component.test.jsx
@@ -17,15 +17,25 @@ describe("widgets/crowdsec/component", () => {
vi.clearAllMocks();
});
- it("selects alerts24h endpoint when limit24h is enabled", () => {
+ it("requests the alerts and bans endpoints", () => {
+ useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined }));
+
+ renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(useWidgetAPI).toHaveBeenNthCalledWith(1, expect.any(Object), "alerts");
+ expect(useWidgetAPI).toHaveBeenNthCalledWith(2, expect.any(Object), "bans");
+ });
+
+ it("ignores the deprecated limit24h option", () => {
useWidgetAPI.mockImplementation(() => ({ data: undefined, error: undefined }));
renderWithProviders(, {
settings: { hideErrors: false },
});
- expect(useWidgetAPI).toHaveBeenNthCalledWith(1, expect.any(Object), "alerts24h");
- expect(useWidgetAPI).toHaveBeenNthCalledWith(2, expect.any(Object), "bans");
+ expect(useWidgetAPI).toHaveBeenNthCalledWith(1, expect.any(Object), "alerts");
});
it("renders placeholders when both alerts and bans are missing", () => {
@@ -52,4 +62,28 @@ describe("widgets/crowdsec/component", () => {
expectBlockValue(container, "crowdsec.alerts", 0);
expectBlockValue(container, "crowdsec.bans", 0);
});
+
+ it("marks the alert count as truncated at the API limit", () => {
+ useWidgetAPI
+ .mockReturnValueOnce({ data: new Array(500).fill({}), error: undefined })
+ .mockReturnValueOnce({ data: [], error: undefined });
+
+ renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expect(screen.getByText("500+")).toBeInTheDocument();
+ });
+
+ it("does not mark the alert count below the API limit", () => {
+ useWidgetAPI
+ .mockReturnValueOnce({ data: new Array(499).fill({}), error: undefined })
+ .mockReturnValueOnce({ data: [], error: undefined });
+
+ const { container } = renderWithProviders(, {
+ settings: { hideErrors: false },
+ });
+
+ expectBlockValue(container, "crowdsec.alerts", 499);
+ });
});
diff --git a/src/widgets/crowdsec/widget.js b/src/widgets/crowdsec/widget.js
index 1b60168d0..223e94811 100644
--- a/src/widgets/crowdsec/widget.js
+++ b/src/widgets/crowdsec/widget.js
@@ -7,13 +7,10 @@ const widget = {
mappings: {
alerts: {
- endpoint: "alerts",
- },
- alerts24h: {
- endpoint: "alerts?limit=0&since=24h",
+ endpoint: "alerts?limit=500&since=24h&with_decisions=false&include_capi=false",
},
bans: {
- endpoint: "alerts?decision_type=ban&origin=crowdsec&has_active_decision=1",
+ endpoint: "alerts?decision_type=ban&include_capi=false&has_active_decision=1&with_decisions=false",
},
},
};
diff --git a/src/widgets/customapi/component.jsx b/src/widgets/customapi/component.jsx
index d034eff42..af94434da 100644
--- a/src/widgets/customapi/component.jsx
+++ b/src/widgets/customapi/component.jsx
@@ -1,8 +1,8 @@
import classNames from "classnames";
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import * as shvl from "utils/config/shvl";
import useWidgetAPI from "utils/proxy/use-widget-api";
diff --git a/src/widgets/deluge/component.jsx b/src/widgets/deluge/component.jsx
index 7b53b0654..cee80a631 100644
--- a/src/widgets/deluge/component.jsx
+++ b/src/widgets/deluge/component.jsx
@@ -1,9 +1,9 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import QueueEntry from "../../components/widgets/queue/queueEntry";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/develancacheui/component.jsx b/src/widgets/develancacheui/component.jsx
index 08811a326..70e34613d 100644
--- a/src/widgets/develancacheui/component.jsx
+++ b/src/widgets/develancacheui/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/diskstation/component.jsx b/src/widgets/diskstation/component.jsx
index d4327ef60..34934a72d 100644
--- a/src/widgets/diskstation/component.jsx
+++ b/src/widgets/diskstation/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/dispatcharr/component.jsx b/src/widgets/dispatcharr/component.jsx
index 77ecccbec..6cde7c127 100644
--- a/src/widgets/dispatcharr/component.jsx
+++ b/src/widgets/dispatcharr/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function StreamEntry({ title, clients, bitrate }) {
diff --git a/src/widgets/docker/component.jsx b/src/widgets/docker/component.jsx
index bb872a0d3..b37e1eff0 100644
--- a/src/widgets/docker/component.jsx
+++ b/src/widgets/docker/component.jsx
@@ -1,10 +1,11 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import useSWR from "swr";
import { calculateCPUPercent, calculateThroughput, calculateUsedMemory } from "./stats-helpers";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
export default function Component({ service }) {
const { t } = useTranslation();
diff --git a/src/widgets/dockhand/component.jsx b/src/widgets/dockhand/component.jsx
index 0fff1c49f..15b96fcf4 100644
--- a/src/widgets/dockhand/component.jsx
+++ b/src/widgets/dockhand/component.jsx
@@ -1,21 +1,17 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-const MAX_FIELDS = 4;
+const DEFAULT_FIELDS = ["running", "total", "cpu", "memory"];
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
- if (!widget.fields) {
- widget.fields = ["running", "total", "cpu", "memory"];
- } else if (widget.fields.length > MAX_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_FIELDS);
- }
-
const { data: stats, error: statsError } = useWidgetAPI(widget, "dashboard/stats");
if (statsError) {
diff --git a/src/widgets/dockhand/component.test.jsx b/src/widgets/dockhand/component.test.jsx
index d9f0be490..f55981239 100644
--- a/src/widgets/dockhand/component.test.jsx
+++ b/src/widgets/dockhand/component.test.jsx
@@ -22,7 +22,11 @@ describe("widgets/dockhand/component", () => {
const service = { widget: { type: "dockhand" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["running", "total", "cpu", "memory"]);
+ expect(service.widget.fields).toBeUndefined();
+ expect(useWidgetAPI).toHaveBeenCalledWith(
+ { type: "dockhand", fields: ["running", "total", "cpu", "memory"] },
+ "dashboard/stats",
+ );
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("dockhand.running")).toBeInTheDocument();
expect(screen.getByText("dockhand.total")).toBeInTheDocument();
diff --git a/src/widgets/downloadstation/component.jsx b/src/widgets/downloadstation/component.jsx
index a18ddce01..2472170bf 100644
--- a/src/widgets/downloadstation/component.jsx
+++ b/src/widgets/downloadstation/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/duplicati/component.jsx b/src/widgets/duplicati/component.jsx
index 90e0253ed..7897892ce 100644
--- a/src/widgets/duplicati/component.jsx
+++ b/src/widgets/duplicati/component.jsx
@@ -1,21 +1,17 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
const DEFAULT_FIELDS = ["jobs", "errors", "lastBackup", "nextRun"];
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
- if (!widget.fields?.length) {
- widget.fields = DEFAULT_FIELDS;
- } else if (widget.fields?.length > 4) {
- widget.fields = widget.fields.slice(0, 4);
- }
-
const { data, error } = useWidgetAPI(widget);
if (error) {
diff --git a/src/widgets/emby/component.jsx b/src/widgets/emby/component.jsx
index 18fed4cc4..940b5718f 100644
--- a/src/widgets/emby/component.jsx
+++ b/src/widgets/emby/component.jsx
@@ -1,9 +1,9 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import { BsCpu, BsFillCpuFill, BsFillPlayFill, BsPauseFill, BsVolumeMuteFill } from "react-icons/bs";
import { MdOutlineSmartDisplay } from "react-icons/md";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { getURLSearchParams } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
diff --git a/src/widgets/esphome/component.jsx b/src/widgets/esphome/component.jsx
index 94fea61fa..071105e6f 100644
--- a/src/widgets/esphome/component.jsx
+++ b/src/widgets/esphome/component.jsx
@@ -1,12 +1,16 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const DEFAULT_FIELDS = ["online", "offline", "offline_alt", "total"];
+
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
const { data: resultData, error: resultError } = useWidgetAPI(widget);
@@ -14,12 +18,6 @@ export default function Component({ service }) {
return ;
}
- if (!widget.fields || widget.fields.length === 0) {
- widget.fields = ["online", "offline", "offline_alt", "total"];
- } else if (widget.fields.length > 4) {
- widget.fields = widget.fields.slice(0, 4);
- }
-
if (!resultData) {
return (
diff --git a/src/widgets/esphome/component.test.jsx b/src/widgets/esphome/component.test.jsx
index 10ad52176..9be60325c 100644
--- a/src/widgets/esphome/component.test.jsx
+++ b/src/widgets/esphome/component.test.jsx
@@ -22,7 +22,7 @@ describe("widgets/esphome/component", () => {
const service = { widget: { type: "esphome" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["online", "offline", "offline_alt", "total"]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("esphome.online")).toBeInTheDocument();
expect(screen.getByText("esphome.offline")).toBeInTheDocument();
diff --git a/src/widgets/evcc/component.jsx b/src/widgets/evcc/component.jsx
index fc638bd46..f6c152554 100644
--- a/src/widgets/evcc/component.jsx
+++ b/src/widgets/evcc/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function toKilowatts(t, value) {
diff --git a/src/widgets/filebrowser/component.jsx b/src/widgets/filebrowser/component.jsx
index 51ae03d97..fd4e00fce 100644
--- a/src/widgets/filebrowser/component.jsx
+++ b/src/widgets/filebrowser/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/fileflows/component.jsx b/src/widgets/fileflows/component.jsx
index 207f11002..46f46c884 100755
--- a/src/widgets/fileflows/component.jsx
+++ b/src/widgets/fileflows/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/firefly/component.jsx b/src/widgets/firefly/component.jsx
index 8e4da639c..28a47e47c 100644
--- a/src/widgets/firefly/component.jsx
+++ b/src/widgets/firefly/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/flood/component.jsx b/src/widgets/flood/component.jsx
index 2d3d9da12..6bfe5abc4 100644
--- a/src/widgets/flood/component.jsx
+++ b/src/widgets/flood/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/freshrss/component.jsx b/src/widgets/freshrss/component.jsx
index a9740cba4..466f3bdff 100644
--- a/src/widgets/freshrss/component.jsx
+++ b/src/widgets/freshrss/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/frigate/component.jsx b/src/widgets/frigate/component.jsx
index 6a342bbb5..5f66f0c56 100644
--- a/src/widgets/frigate/component.jsx
+++ b/src/widgets/frigate/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/fritzbox/component.jsx b/src/widgets/fritzbox/component.jsx
index 5611de312..549f4bb96 100644
--- a/src/widgets/fritzbox/component.jsx
+++ b/src/widgets/fritzbox/component.jsx
@@ -1,13 +1,15 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
export const fritzboxDefaultFields = ["connectionStatus", "uptime", "maxDown", "maxUp"];
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, fritzboxDefaultFields);
const { widget } = service;
const { data: fritzboxData, error: fritzboxError } = useWidgetAPI(widget, "status");
@@ -15,16 +17,6 @@ export default function Component({ service }) {
return ;
}
- // Default fields
- if (!widget.fields?.length > 0) {
- widget.fields = fritzboxDefaultFields;
- }
- const MAX_ALLOWED_FIELDS = 4;
- // Limits max number of displayed fields
- if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
if (!fritzboxData) {
return (
diff --git a/src/widgets/fritzbox/component.test.jsx b/src/widgets/fritzbox/component.test.jsx
index f1abcaf41..017fb200c 100644
--- a/src/widgets/fritzbox/component.test.jsx
+++ b/src/widgets/fritzbox/component.test.jsx
@@ -9,7 +9,7 @@ import { expectBlockValue } from "test-utils/widget-assertions";
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
-import Component, { fritzboxDefaultFields } from "./component";
+import Component from "./component";
describe("widgets/fritzbox/component", () => {
beforeEach(() => {
@@ -22,7 +22,7 @@ describe("widgets/fritzbox/component", () => {
const service = { widget: { type: "fritzbox", url: "http://x" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(fritzboxDefaultFields);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("fritzbox.connectionStatus")).toBeInTheDocument();
expect(screen.getByText("fritzbox.uptime")).toBeInTheDocument();
@@ -43,7 +43,14 @@ describe("widgets/fritzbox/component", () => {
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["down", "up", "received", "sent"]);
+ expect(service.widget.fields).toEqual([
+ "down",
+ "up",
+ "received",
+ "sent",
+ "externalIPAddress",
+ "externalIPv6Prefix",
+ ]);
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("fritzbox.down")).toBeInTheDocument();
expect(screen.getByText("fritzbox.up")).toBeInTheDocument();
diff --git a/src/widgets/gamedig/component.jsx b/src/widgets/gamedig/component.jsx
index 8ed11a124..5b3a75d54 100644
--- a/src/widgets/gamedig/component.jsx
+++ b/src/widgets/gamedig/component.jsx
@@ -1,10 +1,14 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const DEFAULT_FIELDS = ["map", "currentPlayers", "ping"];
+
+export default function Component({ service: configuredService }) {
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
const { data: serverData, error: serverError } = useWidgetAPI(widget, "status");
const { t } = useTranslation();
@@ -13,16 +17,6 @@ export default function Component({ service }) {
return ;
}
- // Default fields
- if (widget.fields == null || widget.fields.length === 0) {
- widget.fields = ["map", "currentPlayers", "ping"];
- }
- const MAX_ALLOWED_FIELDS = 4;
- // Limits max number of displayed fields
- if (widget.fields != null && widget.fields.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
if (!serverData) {
return (
diff --git a/src/widgets/gamedig/component.test.jsx b/src/widgets/gamedig/component.test.jsx
index 895b4d3bc..456b0ec25 100644
--- a/src/widgets/gamedig/component.test.jsx
+++ b/src/widgets/gamedig/component.test.jsx
@@ -22,7 +22,7 @@ describe("widgets/gamedig/component", () => {
const service = { widget: { type: "gamedig", url: "http://x" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["map", "currentPlayers", "ping"]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("gamedig.map")).toBeInTheDocument();
expect(screen.getByText("gamedig.currentPlayers")).toBeInTheDocument();
@@ -54,7 +54,7 @@ describe("widgets/gamedig/component", () => {
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["status", "name", "map", "currentPlayers"]);
+ expect(service.widget.fields).toEqual(["status", "name", "map", "currentPlayers", "ping"]);
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expectBlockValue(container, "gamedig.status", "gamedig.online");
diff --git a/src/widgets/gatus/component.jsx b/src/widgets/gatus/component.jsx
index 6fda47525..198fb08a1 100644
--- a/src/widgets/gatus/component.jsx
+++ b/src/widgets/gatus/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/ghostfolio/component.jsx b/src/widgets/ghostfolio/component.jsx
index 16025b958..f0007f261 100644
--- a/src/widgets/ghostfolio/component.jsx
+++ b/src/widgets/ghostfolio/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function getPerformancePercent(t, performanceRange) {
diff --git a/src/widgets/gitea/component.jsx b/src/widgets/gitea/component.jsx
index 81b69d67d..3b0b078fd 100644
--- a/src/widgets/gitea/component.jsx
+++ b/src/widgets/gitea/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/gitlab/component.jsx b/src/widgets/gitlab/component.jsx
index e0ef49463..75a3bf2fa 100644
--- a/src/widgets/gitlab/component.jsx
+++ b/src/widgets/gitlab/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/glances/components/container.jsx b/src/widgets/glances/components/container.jsx
index 7bcd5c462..744bf0c1c 100644
--- a/src/widgets/glances/components/container.jsx
+++ b/src/widgets/glances/components/container.jsx
@@ -1,9 +1,10 @@
import classNames from "classnames";
import { useContext } from "react";
-import { SettingsContext } from "utils/contexts/settings";
import Error from "./error";
+import { SettingsContext } from "utils/contexts/settings";
+
export default function Container({ children, widget, error = null, chart = true, className = "" }) {
const { settings } = useContext(SettingsContext);
const hideErrors = settings.hideErrors || widget?.hideErrors;
diff --git a/src/widgets/glances/components/custom_tooltip.jsx b/src/widgets/glances/components/custom_tooltip.jsx
index c7f2c7baa..84f297109 100644
--- a/src/widgets/glances/components/custom_tooltip.jsx
+++ b/src/widgets/glances/components/custom_tooltip.jsx
@@ -3,7 +3,7 @@ export default function Tooltip({ active, payload, formatter }) {
return (
{payload.map((pld, id) => (
-
+
{formatter(pld.value)} {payload[id].name}
diff --git a/src/widgets/glances/metrics/containers.jsx b/src/widgets/glances/metrics/containers.jsx
index f9cc1e0e1..de692b679 100644
--- a/src/widgets/glances/metrics/containers.jsx
+++ b/src/widgets/glances/metrics/containers.jsx
@@ -1,9 +1,9 @@
-import ResolvedIcon from "components/resolvedicon";
import { useTranslation } from "next-i18next/pages";
import Block from "../components/block";
import Container from "../components/container";
+import ResolvedIcon from "components/resolvedicon";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -41,7 +41,7 @@ export default function Component({ service }) {
);
}
- data.splice(chart ? 5 : 1);
+ const visibleData = data.slice(0, chart ? 5 : 1);
let headerYPosition = "top-4";
let listYPosition = "bottom-4";
if (chart) {
@@ -61,7 +61,7 @@ export default function Component({ service }) {
- {data.map((item) => (
+ {visibleData.map((item) => (
{statusMap[item[statusKey]]}
diff --git a/src/widgets/glances/metrics/containers.test.jsx b/src/widgets/glances/metrics/containers.test.jsx
index 58889f1db..0124744cc 100644
--- a/src/widgets/glances/metrics/containers.test.jsx
+++ b/src/widgets/glances/metrics/containers.test.jsx
@@ -103,5 +103,6 @@ describe("widgets/glances/metrics/containers", () => {
expect(screen.getByText("item-0")).toBeInTheDocument();
expect(screen.getByText("item-4")).toBeInTheDocument();
expect(screen.queryByText("item-5")).not.toBeInTheDocument();
+ expect(data).toHaveLength(6);
});
});
diff --git a/src/widgets/glances/metrics/cpu.jsx b/src/widgets/glances/metrics/cpu.jsx
index 0e3b334df..81d450c2b 100644
--- a/src/widgets/glances/metrics/cpu.jsx
+++ b/src/widgets/glances/metrics/cpu.jsx
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
-import { useEffect, useState } from "react";
+import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
+import useDataPoints from "./use-data-points";
+
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -19,26 +21,26 @@ export default function Component({ service }) {
const { chart, refreshInterval = defaultInterval, pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
- const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
+ const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, { value: 0 });
- const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/cpu`, {
- refreshInterval: Math.max(defaultInterval, refreshInterval),
- });
+ const handleData = useCallback(
+ (newData) => {
+ if (newData) addDataPoint({ value: newData.total });
+ },
+ [addDataPoint],
+ );
+
+ const { data, error } = useWidgetAPI(
+ service.widget,
+ `${apiVersion}/cpu`,
+ {
+ refreshInterval: Math.max(defaultInterval, refreshInterval),
+ },
+ { onSuccess: handleData },
+ );
const { data: quicklookData, error: quicklookError } = useWidgetAPI(service.widget, `${apiVersion}/quicklook`);
- useEffect(() => {
- if (data) {
- setDataPoints((prevDataPoints) => {
- const newDataPoints = [...prevDataPoints, { value: data.total }];
- if (newDataPoints.length > pointsLimit) {
- newDataPoints.shift();
- }
- return newDataPoints;
- });
- }
- }, [data, pointsLimit]);
-
if (error) {
return
;
}
diff --git a/src/widgets/glances/metrics/disk.jsx b/src/widgets/glances/metrics/disk.jsx
index 89ce1cc22..770597338 100644
--- a/src/widgets/glances/metrics/disk.jsx
+++ b/src/widgets/glances/metrics/disk.jsx
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
-import { useEffect, useState } from "react";
+import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
+import useDataPoints from "./use-data-points";
+
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -20,39 +22,37 @@ export default function Component({ service }) {
const apiVersion = parseVersionForUrl(version, 3);
const [, diskName] = widget.metric.split(":");
- const [dataPoints, setDataPoints] = useState(
- new Array(pointsLimit).fill({ read_bytes: 0, write_bytes: 0, time_since_update: 0 }, 0, pointsLimit),
- );
- const [ratePoints, setRatePoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
-
- const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/diskio`, {
- refreshInterval: Math.max(defaultInterval, refreshInterval),
+ const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, {
+ read_bytes: 0,
+ write_bytes: 0,
+ time_since_update: 0,
});
+ const handleData = useCallback(
+ (newData) => {
+ if (!newData?.error) {
+ const diskData = newData.find((item) => item.disk_name === diskName);
+ if (diskData) addDataPoint(diskData);
+ }
+ },
+ [addDataPoint, diskName],
+ );
+
+ const { data, error } = useWidgetAPI(
+ service.widget,
+ `${apiVersion}/diskio`,
+ {
+ refreshInterval: Math.max(defaultInterval, refreshInterval),
+ },
+ { onSuccess: handleData },
+ );
+
const calculateRates = (d) =>
d.map((item) => ({
a: item.read_bytes / item.time_since_update,
b: item.write_bytes / item.time_since_update,
}));
- useEffect(() => {
- if (data && !data.error) {
- const diskData = data.find((item) => item.disk_name === diskName);
-
- setDataPoints((prevDataPoints) => {
- const newDataPoints = [...prevDataPoints, diskData];
- if (newDataPoints.length > pointsLimit) {
- newDataPoints.shift();
- }
- return newDataPoints;
- });
- }
- }, [data, diskName, pointsLimit]);
-
- useEffect(() => {
- setRatePoints(calculateRates(dataPoints));
- }, [dataPoints]);
-
if (error || (data && data.error)) {
const finalError = error || data.error;
return
;
@@ -83,7 +83,7 @@ export default function Component({ service }) {
{chart && (
diff --git a/src/widgets/glances/metrics/gpu.jsx b/src/widgets/glances/metrics/gpu.jsx
index 4c99e698d..aa3b6629b 100644
--- a/src/widgets/glances/metrics/gpu.jsx
+++ b/src/widgets/glances/metrics/gpu.jsx
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
-import { useEffect, useState } from "react";
+import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
+import useDataPoints from "./use-data-points";
+
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -20,27 +22,26 @@ export default function Component({ service }) {
const apiVersion = parseVersionForUrl(version, 3);
const [, gpuName] = widget.metric.split(":");
- const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ a: 0, b: 0 }, 0, pointsLimit));
+ const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, { a: 0, b: 0 });
- const { data, error } = useWidgetAPI(widget, `${apiVersion}/gpu`, {
- refreshInterval: Math.max(defaultInterval, refreshInterval),
- });
-
- useEffect(() => {
- if (data && !data.error) {
- const gpuData = data.find((item) => item[item.key] == gpuName);
-
- if (gpuData) {
- setDataPoints((prevDataPoints) => {
- const newDataPoints = [...prevDataPoints, { a: gpuData.mem, b: gpuData.proc }];
- if (newDataPoints.length > pointsLimit) {
- newDataPoints.shift();
- }
- return newDataPoints;
- });
+ const handleData = useCallback(
+ (newData) => {
+ if (!newData?.error) {
+ const gpuData = newData.find((item) => item[item.key] == gpuName);
+ if (gpuData) addDataPoint({ a: gpuData.mem, b: gpuData.proc });
}
- }
- }, [data, gpuName, pointsLimit]);
+ },
+ [addDataPoint, gpuName],
+ );
+
+ const { data, error } = useWidgetAPI(
+ widget,
+ `${apiVersion}/gpu`,
+ {
+ refreshInterval: Math.max(defaultInterval, refreshInterval),
+ },
+ { onSuccess: handleData },
+ );
if (error || (data && data.error)) {
const finalError = error || data.error;
diff --git a/src/widgets/glances/metrics/memory.jsx b/src/widgets/glances/metrics/memory.jsx
index c8d0e6207..1fe9b1ae0 100644
--- a/src/widgets/glances/metrics/memory.jsx
+++ b/src/widgets/glances/metrics/memory.jsx
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
-import { useEffect, useState } from "react";
+import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
+import useDataPoints from "./use-data-points";
+
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -20,23 +22,23 @@ export default function Component({ service }) {
const { refreshInterval = defaultInterval(chart), pointsLimit = defaultPointsLimit, version = 3 } = widget;
const apiVersion = parseVersionForUrl(version, 3);
- const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
+ const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, { a: 0, b: 0 });
- const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/mem`, {
- refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
- });
+ const handleData = useCallback(
+ (newData) => {
+ if (newData) addDataPoint({ a: newData.used, b: newData.available });
+ },
+ [addDataPoint],
+ );
- useEffect(() => {
- if (data) {
- setDataPoints((prevDataPoints) => {
- const newDataPoints = [...prevDataPoints, { a: data.used, b: data.available }];
- if (newDataPoints.length > pointsLimit) {
- newDataPoints.shift();
- }
- return newDataPoints;
- });
- }
- }, [data, pointsLimit]);
+ const { data, error } = useWidgetAPI(
+ service.widget,
+ `${apiVersion}/mem`,
+ {
+ refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
+ },
+ { onSuccess: handleData },
+ );
if (error) {
return ;
diff --git a/src/widgets/glances/metrics/net.jsx b/src/widgets/glances/metrics/net.jsx
index 914075008..70d3f26c6 100644
--- a/src/widgets/glances/metrics/net.jsx
+++ b/src/widgets/glances/metrics/net.jsx
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
-import { useEffect, useState } from "react";
+import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
+import useDataPoints from "./use-data-points";
+
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -25,33 +27,32 @@ export default function Component({ service }) {
const [, interfaceName] = metric.split(":");
- const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
+ const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, { a: 0, b: 0 });
- const { data, error } = useWidgetAPI(widget, `${apiVersion}/network`, {
- refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
- });
+ const handleData = useCallback(
+ (newData) => {
+ if (!newData?.error) {
+ const interfaceData = newData.find((item) => item[item.key] === interfaceName);
- useEffect(() => {
- if (data && !data.error) {
- const interfaceData = data.find((item) => item[item.key] === interfaceName);
-
- if (interfaceData) {
- setDataPoints((prevDataPoints) => {
- const newDataPoints = [
- ...prevDataPoints,
- {
- a: (interfaceData[rxKey] * 8) / interfaceData.time_since_update,
- b: (interfaceData[txKey] * 8) / interfaceData.time_since_update,
- },
- ];
- if (newDataPoints.length > pointsLimit) {
- newDataPoints.shift();
- }
- return newDataPoints;
- });
+ if (interfaceData) {
+ addDataPoint({
+ a: (interfaceData[rxKey] * 8) / interfaceData.time_since_update,
+ b: (interfaceData[txKey] * 8) / interfaceData.time_since_update,
+ });
+ }
}
- }
- }, [data, interfaceName, pointsLimit, rxKey, txKey]);
+ },
+ [addDataPoint, interfaceName, rxKey, txKey],
+ );
+
+ const { data, error } = useWidgetAPI(
+ widget,
+ `${apiVersion}/network`,
+ {
+ refreshInterval: Math.max(defaultInterval(chart), refreshInterval),
+ },
+ { onSuccess: handleData },
+ );
if (error || (data && data.error)) {
const finalError = error || data.error;
diff --git a/src/widgets/glances/metrics/process.jsx b/src/widgets/glances/metrics/process.jsx
index 62f0f985d..7c26153a7 100644
--- a/src/widgets/glances/metrics/process.jsx
+++ b/src/widgets/glances/metrics/process.jsx
@@ -1,9 +1,9 @@
-import ResolvedIcon from "components/resolvedicon";
import { useTranslation } from "next-i18next/pages";
import Block from "../components/block";
import Container from "../components/container";
+import ResolvedIcon from "components/resolvedicon";
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -43,7 +43,7 @@ export default function Component({ service }) {
);
}
- data.splice(chart ? 5 : 1);
+ const visibleData = data.slice(0, chart ? 5 : 1);
let headerYPosition = "top-4";
let listYPosition = "bottom-4";
if (chart) {
@@ -63,7 +63,7 @@ export default function Component({ service }) {
- {data.map((item) => (
+ {visibleData.map((item) => (
{statusMap[item.status]}
diff --git a/src/widgets/glances/metrics/process.test.jsx b/src/widgets/glances/metrics/process.test.jsx
index 9a3ded419..f9f8eab93 100644
--- a/src/widgets/glances/metrics/process.test.jsx
+++ b/src/widgets/glances/metrics/process.test.jsx
@@ -19,4 +19,20 @@ describe("widgets/glances/metrics/process", () => {
});
expect(screen.getByText("-")).toBeInTheDocument();
});
+
+ it("limits displayed processes without modifying the response", () => {
+ const data = [
+ { pid: 1, status: "R", name: "first", cpu_percent: 1, memory_info: [100] },
+ { pid: 2, status: "S", name: "second", cpu_percent: 2, memory_info: [200] },
+ ];
+ useWidgetAPI.mockReturnValue({ data, error: undefined });
+
+ renderWithProviders(
, {
+ settings: { hideErrors: false },
+ });
+
+ expect(screen.getByText("first")).toBeInTheDocument();
+ expect(screen.queryByText("second")).not.toBeInTheDocument();
+ expect(data).toHaveLength(2);
+ });
});
diff --git a/src/widgets/glances/metrics/sensor.jsx b/src/widgets/glances/metrics/sensor.jsx
index ef744a816..89f96a999 100644
--- a/src/widgets/glances/metrics/sensor.jsx
+++ b/src/widgets/glances/metrics/sensor.jsx
@@ -1,10 +1,12 @@
import { useTranslation } from "next-i18next/pages";
import dynamic from "next/dynamic";
-import { useEffect, useState } from "react";
+import { useCallback } from "react";
import Block from "../components/block";
import Container from "../components/container";
+import useDataPoints from "./use-data-points";
+
import { parseVersionForUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
@@ -20,32 +22,29 @@ export default function Component({ service }) {
const apiVersion = parseVersionForUrl(version, 3);
const [, sensorName] = widget.metric.split(":");
- const [dataPoints, setDataPoints] = useState(new Array(pointsLimit).fill({ value: 0 }, 0, pointsLimit));
+ const [dataPoints, addDataPoint] = useDataPoints(pointsLimit, { value: 0 });
- const { data, error } = useWidgetAPI(service.widget, `${apiVersion}/sensors`, {
- refreshInterval: Math.max(defaultInterval, refreshInterval),
- });
-
- useEffect(() => {
- if (data && !data.error) {
- const sensorData = data.find((item) => item.label === sensorName);
- if (sensorData) {
- setDataPoints((prevDataPoints) => {
- const newDataPoints = [...prevDataPoints, { value: sensorData.value }];
- if (newDataPoints.length > pointsLimit) {
- newDataPoints.shift();
- }
- return newDataPoints;
- });
- } else {
- data.error = true;
+ const handleData = useCallback(
+ (newData) => {
+ if (!newData?.error) {
+ const sensorData = newData.find((item) => item.label === sensorName);
+ if (sensorData) addDataPoint({ value: sensorData.value });
}
- }
- }, [data, sensorName, pointsLimit]);
+ },
+ [addDataPoint, sensorName],
+ );
- if (error || (data && data.error)) {
- const finalError = error || data.error;
- return
;
+ const { data, error } = useWidgetAPI(
+ service.widget,
+ `${apiVersion}/sensors`,
+ {
+ refreshInterval: Math.max(defaultInterval, refreshInterval),
+ },
+ { onSuccess: handleData },
+ );
+
+ if (error || data?.error) {
+ return
;
}
if (!data) {
diff --git a/src/widgets/glances/metrics/sensor.test.jsx b/src/widgets/glances/metrics/sensor.test.jsx
index 4dee7a9cb..6f7f0b658 100644
--- a/src/widgets/glances/metrics/sensor.test.jsx
+++ b/src/widgets/glances/metrics/sensor.test.jsx
@@ -22,4 +22,17 @@ describe("widgets/glances/metrics/sensor", () => {
);
expect(screen.getByText("-")).toBeInTheDocument();
});
+
+ it("renders a placeholder for a missing sensor without modifying the response", () => {
+ const data = [{ label: "Other", value: 42 }];
+ useWidgetAPI.mockReturnValue({ data, error: undefined });
+
+ renderWithProviders(
+
,
+ { settings: { hideErrors: false } },
+ );
+
+ expect(screen.getByText("-")).toBeInTheDocument();
+ expect(data).toEqual([{ label: "Other", value: 42 }]);
+ });
});
diff --git a/src/widgets/glances/metrics/use-data-points.js b/src/widgets/glances/metrics/use-data-points.js
new file mode 100644
index 000000000..8763317ea
--- /dev/null
+++ b/src/widgets/glances/metrics/use-data-points.js
@@ -0,0 +1,17 @@
+import { useCallback, useState } from "react";
+
+export default function useDataPoints(pointsLimit, initialPoint) {
+ const [dataPoints, setDataPoints] = useState(() => new Array(pointsLimit).fill(initialPoint));
+
+ const addDataPoint = useCallback(
+ (dataPoint) => {
+ setDataPoints((currentDataPoints) => {
+ if (pointsLimit <= 0) return [];
+ return [...currentDataPoints, dataPoint].slice(-pointsLimit);
+ });
+ },
+ [pointsLimit],
+ );
+
+ return [dataPoints, addDataPoint];
+}
diff --git a/src/widgets/glances/metrics/use-data-points.test.jsx b/src/widgets/glances/metrics/use-data-points.test.jsx
new file mode 100644
index 000000000..b6bf8b314
--- /dev/null
+++ b/src/widgets/glances/metrics/use-data-points.test.jsx
@@ -0,0 +1,23 @@
+// @vitest-environment jsdom
+
+import { act, renderHook } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import useDataPoints from "./use-data-points";
+
+describe("widgets/glances/metrics/use-data-points", () => {
+ it("appends data points while keeping the requested history length", () => {
+ const { result, rerender } = renderHook(({ pointsLimit }) => useDataPoints(pointsLimit, { value: 0 }), {
+ initialProps: { pointsLimit: 3 },
+ });
+
+ expect(result.current[0]).toEqual([{ value: 0 }, { value: 0 }, { value: 0 }]);
+
+ act(() => result.current[1]({ value: 1 }));
+ expect(result.current[0]).toEqual([{ value: 0 }, { value: 0 }, { value: 1 }]);
+
+ rerender({ pointsLimit: 2 });
+ act(() => result.current[1]({ value: 2 }));
+ expect(result.current[0]).toEqual([{ value: 1 }, { value: 2 }]);
+ });
+});
diff --git a/src/widgets/gluetun/component.jsx b/src/widgets/gluetun/component.jsx
index 2a8401c68..783b1aef3 100644
--- a/src/widgets/gluetun/component.jsx
+++ b/src/widgets/gluetun/component.jsx
@@ -1,15 +1,14 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const DEFAULT_FIELDS = ["public_ip", "region", "country"];
+
+export default function Component({ service: configuredService }) {
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
- if (!widget.fields) {
- widget.fields = ["public_ip", "region", "country"];
- }
-
const { data: gluetunData, error: gluetunError } = useWidgetAPI(widget, "ip");
const includePF = widget.fields.includes("port_forwarded");
const pfEndpoint = widget.version > 1 ? "port_forwarded_v2" : "port_forwarded";
diff --git a/src/widgets/gluetun/component.test.jsx b/src/widgets/gluetun/component.test.jsx
index aeb617493..6ee682733 100644
--- a/src/widgets/gluetun/component.test.jsx
+++ b/src/widgets/gluetun/component.test.jsx
@@ -22,7 +22,7 @@ describe("widgets/gluetun/component", () => {
const service = { widget: { type: "gluetun", url: "http://x" } };
const { container } = renderWithProviders(
, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["public_ip", "region", "country"]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("gluetun.public_ip")).toBeInTheDocument();
expect(screen.getByText("gluetun.region")).toBeInTheDocument();
diff --git a/src/widgets/gotify/component.jsx b/src/widgets/gotify/component.jsx
index 6cf9cea94..f5cadb06b 100644
--- a/src/widgets/gotify/component.jsx
+++ b/src/widgets/gotify/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/grafana/component.jsx b/src/widgets/grafana/component.jsx
index a2924cbc9..f01ff49a5 100755
--- a/src/widgets/grafana/component.jsx
+++ b/src/widgets/grafana/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/hdhomerun/component.jsx b/src/widgets/hdhomerun/component.jsx
index 2532f92bc..d87ee471d 100644
--- a/src/widgets/hdhomerun/component.jsx
+++ b/src/widgets/hdhomerun/component.jsx
@@ -1,9 +1,12 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const DEFAULT_FIELDS = ["channels", "hd"];
+
+export default function Component({ service: configuredService }) {
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
const { tuner = 0 } = widget;
@@ -24,15 +27,6 @@ export default function Component({ service }) {
);
}
- // Provide a default if not set in the config
- if (!widget.fields) {
- widget.fields = ["channels", "hd"];
- }
- // Limit to a maximum of 4 at a time
- if (widget.fields.length > 4) {
- widget.fields = widget.fields.slice(0, 4);
- }
-
return (
diff --git a/src/widgets/hdhomerun/component.test.jsx b/src/widgets/hdhomerun/component.test.jsx
index b0afac7b4..0bae6812b 100644
--- a/src/widgets/hdhomerun/component.test.jsx
+++ b/src/widgets/hdhomerun/component.test.jsx
@@ -58,7 +58,7 @@ describe("widgets/hdhomerun/component", () => {
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["channels", "hd", "tunerCount", "channelNumber"]);
+ expect(service.widget.fields).toEqual(["channels", "hd", "tunerCount", "channelNumber", "signalStrength"]);
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("hdhomerun.channels")).toBeInTheDocument();
expect(screen.getByText("hdhomerun.hd")).toBeInTheDocument();
diff --git a/src/widgets/headscale/component.jsx b/src/widgets/headscale/component.jsx
index 2d69feedb..4418fcd57 100644
--- a/src/widgets/headscale/component.jsx
+++ b/src/widgets/headscale/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/healthchecks/component.jsx b/src/widgets/healthchecks/component.jsx
index bacefca2c..ede57747c 100644
--- a/src/widgets/healthchecks/component.jsx
+++ b/src/widgets/healthchecks/component.jsx
@@ -1,9 +1,9 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import { i18n } from "../../../next-i18next.config";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function formatDate(dateString) {
diff --git a/src/widgets/homeassistant/component.jsx b/src/widgets/homeassistant/component.jsx
index 1df415ae2..e43a08b6b 100644
--- a/src/widgets/homeassistant/component.jsx
+++ b/src/widgets/homeassistant/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/homebox/component.jsx b/src/widgets/homebox/component.jsx
index 451d5a9a6..c06d06d60 100644
--- a/src/widgets/homebox/component.jsx
+++ b/src/widgets/homebox/component.jsx
@@ -1,13 +1,15 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
export const homeboxDefaultFields = ["items", "locations", "totalValue"];
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, homeboxDefaultFields);
const { widget } = service;
const { data: homeboxData, error: homeboxError } = useWidgetAPI(widget);
@@ -15,16 +17,6 @@ export default function Component({ service }) {
return ;
}
- // Default fields
- if (!widget.fields?.length > 0) {
- widget.fields = homeboxDefaultFields;
- }
- const MAX_ALLOWED_FIELDS = 4;
- // Limits max number of displayed fields
- if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
if (!homeboxData) {
return (
diff --git a/src/widgets/homebox/component.test.jsx b/src/widgets/homebox/component.test.jsx
index 0342e8392..2113a28c3 100644
--- a/src/widgets/homebox/component.test.jsx
+++ b/src/widgets/homebox/component.test.jsx
@@ -9,7 +9,7 @@ import { expectBlockValue } from "test-utils/widget-assertions";
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
-import Component, { homeboxDefaultFields } from "./component";
+import Component from "./component";
describe("widgets/homebox/component", () => {
beforeEach(() => {
@@ -22,7 +22,7 @@ describe("widgets/homebox/component", () => {
const service = { widget: { type: "homebox", url: "http://x" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(homeboxDefaultFields);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("homebox.items")).toBeInTheDocument();
expect(screen.getByText("homebox.locations")).toBeInTheDocument();
diff --git a/src/widgets/homebridge/component.jsx b/src/widgets/homebridge/component.jsx
index 458dfc8a9..6dde67e28 100644
--- a/src/widgets/homebridge/component.jsx
+++ b/src/widgets/homebridge/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/iframe/component.jsx b/src/widgets/iframe/component.jsx
index 96dd69432..380e6977b 100644
--- a/src/widgets/iframe/component.jsx
+++ b/src/widgets/iframe/component.jsx
@@ -1,7 +1,8 @@
import classNames from "classnames";
-import Container from "components/services/widget/container";
import { useEffect, useState } from "react";
+import Container from "components/services/widget/container";
+
export default function Component({ service }) {
const [refreshTimer, setRefreshTimer] = useState(0);
diff --git a/src/widgets/immich/component.jsx b/src/widgets/immich/component.jsx
index 93cec3d73..cfbd7018d 100644
--- a/src/widgets/immich/component.jsx
+++ b/src/widgets/immich/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/jackett/component.jsx b/src/widgets/jackett/component.jsx
index e27e5dd80..4df28f484 100644
--- a/src/widgets/jackett/component.jsx
+++ b/src/widgets/jackett/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/jdownloader/component.jsx b/src/widgets/jdownloader/component.jsx
index f1b96b98f..c529dbd99 100644
--- a/src/widgets/jdownloader/component.jsx
+++ b/src/widgets/jdownloader/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/jellyfin/component.jsx b/src/widgets/jellyfin/component.jsx
index 8c32bb69c..40158d0a4 100644
--- a/src/widgets/jellyfin/component.jsx
+++ b/src/widgets/jellyfin/component.jsx
@@ -1,11 +1,14 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import { BsCpu, BsFillCpuFill, BsFillPlayFill, BsPauseFill, BsVolumeMuteFill } from "react-icons/bs";
import { MdOutlineSmartDisplay } from "react-icons/md";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { getURLSearchParams } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
+
+const DEFAULT_FIELDS = ["movies", "series", "episodes", "songs"];
function ticksToTime(ticks) {
const milliseconds = ticks / 10000;
@@ -178,12 +181,6 @@ function CountBlocks({ service, countData }) {
const { t } = useTranslation();
const { widget } = service;
- if (!widget.fields || widget.fields.length === 0) {
- widget.fields = ["movies", "series", "episodes", "songs"];
- } else if (widget.fields?.length > 4) {
- widget.fields = widget.fields.slice(0, 4);
- }
-
if (!countData) {
return (
@@ -207,9 +204,10 @@ function CountBlocks({ service, countData }) {
);
}
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
const version = widget?.version ?? 1;
const useJellyfinV2 = version === 2;
diff --git a/src/widgets/jellyfin/component.test.jsx b/src/widgets/jellyfin/component.test.jsx
index fabcd1609..d0701e0a2 100644
--- a/src/widgets/jellyfin/component.test.jsx
+++ b/src/widgets/jellyfin/component.test.jsx
@@ -109,7 +109,7 @@ describe("widgets/jellyfin/component", () => {
renderWithProviders();
- expect(service.widget.fields).toEqual(["movies", "series", "episodes", "albums"]);
+ expect(service.widget.fields).toEqual(["movies", "series", "episodes", "albums", "songs"]);
expect(screen.getByText("jellyfin.albums")).toBeInTheDocument();
expect(screen.getByText("5")).toBeInTheDocument();
expect(screen.queryByText("jellyfin.songs")).not.toBeInTheDocument();
diff --git a/src/widgets/jellystat/component.jsx b/src/widgets/jellystat/component.jsx
index d62e643cd..b11c77c15 100644
--- a/src/widgets/jellystat/component.jsx
+++ b/src/widgets/jellystat/component.jsx
@@ -1,15 +1,14 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
-export default function Component({ service }) {
- const { widget } = service;
+export default function Component({ service: configuredService }) {
+ const configuredDays = configuredService.widget.days;
+ const days = Number.isInteger(configuredDays) && configuredDays > 0 ? configuredDays : 30;
+ const widget = { ...configuredService.widget, days };
+ const service = { ...configuredService, widget };
- // Days validation
- if (!(Number.isInteger(widget.days) && 0 < widget.days)) widget.days = 30;
-
- const { data: viewsData, error: viewsError } = useWidgetAPI(widget, "getViewsByLibraryType", { days: widget.days });
+ const { data: viewsData, error: viewsError } = useWidgetAPI(widget, "getViewsByLibraryType", { days });
const error = viewsError || viewsData?.message;
if (error) {
diff --git a/src/widgets/jellystat/component.test.jsx b/src/widgets/jellystat/component.test.jsx
index ecb822496..fad7de251 100644
--- a/src/widgets/jellystat/component.test.jsx
+++ b/src/widgets/jellystat/component.test.jsx
@@ -26,8 +26,8 @@ describe("widgets/jellystat/component", () => {
const service = { widget: { type: "jellystat", days: -1 } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.days).toBe(30);
- expect(useWidgetAPI).toHaveBeenCalledWith(service.widget, "getViewsByLibraryType", { days: 30 });
+ expect(service.widget.days).toBe(-1);
+ expect(useWidgetAPI).toHaveBeenCalledWith({ type: "jellystat", days: 30 }, "getViewsByLibraryType", { days: 30 });
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("jellystat.songs")).toBeInTheDocument();
diff --git a/src/widgets/karakeep/component.jsx b/src/widgets/karakeep/component.jsx
index 69b47c714..59d3847c9 100644
--- a/src/widgets/karakeep/component.jsx
+++ b/src/widgets/karakeep/component.jsx
@@ -1,14 +1,15 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
export const karakeepDefaultFields = ["bookmarks", "favorites", "archived", "highlights"];
-const MAX_ALLOWED_FIELDS = 4;
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, karakeepDefaultFields);
const { widget } = service;
const { data: statsData, error: statsError } = useWidgetAPI(widget, "stats");
@@ -17,12 +18,6 @@ export default function Component({ service }) {
return ;
}
- if (!widget.fields || widget.fields.length === 0) {
- widget.fields = karakeepDefaultFields;
- } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
if (!statsData) {
return (
diff --git a/src/widgets/karakeep/component.test.jsx b/src/widgets/karakeep/component.test.jsx
index 6b95e71e8..02a8977ae 100644
--- a/src/widgets/karakeep/component.test.jsx
+++ b/src/widgets/karakeep/component.test.jsx
@@ -9,7 +9,7 @@ import { expectBlockValue } from "test-utils/widget-assertions";
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
-import Component, { karakeepDefaultFields } from "./component";
+import Component from "./component";
describe("widgets/karakeep/component", () => {
beforeEach(() => {
@@ -22,7 +22,7 @@ describe("widgets/karakeep/component", () => {
const service = { widget: { type: "karakeep", url: "http://x" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(karakeepDefaultFields);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("karakeep.bookmarks")).toBeInTheDocument();
expect(screen.getByText("karakeep.favorites")).toBeInTheDocument();
@@ -38,7 +38,7 @@ describe("widgets/karakeep/component", () => {
const service = { widget: { type: "karakeep", fields: ["tags", "lists", "bookmarks", "favorites", "archived"] } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["tags", "lists", "bookmarks", "favorites"]);
+ expect(service.widget.fields).toEqual(["tags", "lists", "bookmarks", "favorites", "archived"]);
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("karakeep.tags")).toBeInTheDocument();
expect(screen.getByText("karakeep.lists")).toBeInTheDocument();
diff --git a/src/widgets/kavita/component.jsx b/src/widgets/kavita/component.jsx
index a2a8b5a84..322ac5edf 100644
--- a/src/widgets/kavita/component.jsx
+++ b/src/widgets/kavita/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/komga/component.jsx b/src/widgets/komga/component.jsx
index 968de42c0..f28e0e79f 100644
--- a/src/widgets/komga/component.jsx
+++ b/src/widgets/komga/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/komodo/component.jsx b/src/widgets/komodo/component.jsx
index b1d9e0c64..14fd80392 100644
--- a/src/widgets/komodo/component.jsx
+++ b/src/widgets/komodo/component.jsx
@@ -1,13 +1,23 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-const MAX_ALLOWED_FIELDS = 4;
+const SUMMARY_FIELDS = ["servers", "stacks", "containers"];
+const STACK_FIELDS = ["total", "running", "down", "unhealthy"];
+const CONTAINER_FIELDS = ["total", "running", "stopped", "unhealthy"];
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const configuredWidget = configuredService.widget;
+ const defaultFields = configuredWidget.showSummary
+ ? SUMMARY_FIELDS
+ : configuredWidget.showStacks
+ ? STACK_FIELDS
+ : CONTAINER_FIELDS;
+ const service = withWidgetFields(configuredService, defaultFields);
const { widget } = service;
const containersEndpoint = !(!widget.showSummary && widget.showStacks) ? "containers" : "";
const { data: containersData, error: containersError } = useWidgetAPI(widget, containersEndpoint);
@@ -20,16 +30,6 @@ export default function Component({ service }) {
return ;
}
- if (!widget.fields || widget.fields.length === 0) {
- widget.fields = widget.showSummary
- ? ["servers", "stacks", "containers"]
- : widget.showStacks
- ? ["total", "running", "down", "unhealthy"]
- : ["total", "running", "stopped", "unhealthy"];
- } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
if (
(!widget.showStacks && !containersData) ||
(widget.showSummary && (!containersData || !stacksData || !serversData)) ||
diff --git a/src/widgets/komodo/component.test.jsx b/src/widgets/komodo/component.test.jsx
index 55856eabc..4900d5e40 100644
--- a/src/widgets/komodo/component.test.jsx
+++ b/src/widgets/komodo/component.test.jsx
@@ -25,7 +25,7 @@ describe("widgets/komodo/component", () => {
const service = { widget: { type: "komodo", showStacks: true, showSummary: false } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["total", "running", "down", "unhealthy"]);
+ expect(service.widget.fields).toBeUndefined();
expect(useWidgetAPI.mock.calls[0][1]).toBe(""); // containersEndpoint
expect(useWidgetAPI.mock.calls[1][1]).toBe("stacks");
expect(useWidgetAPI.mock.calls[2][1]).toBe(""); // serversEndpoint
@@ -39,6 +39,20 @@ describe("widgets/komodo/component", () => {
expect(screen.queryByText("komodo.unknown")).toBeNull();
});
+ it("defaults fields for the containers view without modifying the service", () => {
+ useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
+
+ const service = { widget: { type: "komodo", showStacks: false, showSummary: false } };
+ const { container } = renderWithProviders(, { settings: { hideErrors: false } });
+
+ expect(service.widget.fields).toBeUndefined();
+ expect(container.querySelectorAll(".service-block")).toHaveLength(4);
+ expect(screen.getByText("komodo.total")).toBeInTheDocument();
+ expect(screen.getByText("komodo.running")).toBeInTheDocument();
+ expect(screen.getByText("komodo.stopped")).toBeInTheDocument();
+ expect(screen.getByText("komodo.unhealthy")).toBeInTheDocument();
+ });
+
it("renders computed down=stopped+down for stacks view", () => {
useWidgetAPI
.mockReturnValueOnce({ data: undefined, error: undefined }) // containers (disabled)
diff --git a/src/widgets/kopia/component.jsx b/src/widgets/kopia/component.jsx
index c0ba78da6..424b99157 100755
--- a/src/widgets/kopia/component.jsx
+++ b/src/widgets/kopia/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function relativeDate(date) {
diff --git a/src/widgets/kubernetes/component.jsx b/src/widgets/kubernetes/component.jsx
index 6bae67f2e..0742529d5 100644
--- a/src/widgets/kubernetes/component.jsx
+++ b/src/widgets/kubernetes/component.jsx
@@ -1,8 +1,9 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import useSWR from "swr";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
export default function Component({ service }) {
const { t } = useTranslation();
diff --git a/src/widgets/lidarr/component.jsx b/src/widgets/lidarr/component.jsx
index 161f00c98..8de556803 100644
--- a/src/widgets/lidarr/component.jsx
+++ b/src/widgets/lidarr/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/linkwarden/component.jsx b/src/widgets/linkwarden/component.jsx
index 4bf449470..3ae7e8c19 100644
--- a/src/widgets/linkwarden/component.jsx
+++ b/src/widgets/linkwarden/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/lubelogger/component.jsx b/src/widgets/lubelogger/component.jsx
index 967924619..b84af3b7e 100644
--- a/src/widgets/lubelogger/component.jsx
+++ b/src/widgets/lubelogger/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/mailcow/component.jsx b/src/widgets/mailcow/component.jsx
index af2f863d5..b460d1c98 100644
--- a/src/widgets/mailcow/component.jsx
+++ b/src/widgets/mailcow/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/maintainerr/component.jsx b/src/widgets/maintainerr/component.jsx
index 83600afe5..d62b4d30c 100644
--- a/src/widgets/maintainerr/component.jsx
+++ b/src/widgets/maintainerr/component.jsx
@@ -1,22 +1,17 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
const DEFAULT_FIELDS = ["itemsHandled", "episodesHandled", "moviesHandled", "reclaimable"];
-const MAX_FIELDS = 4;
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
- if (!widget.fields?.length) {
- widget.fields = DEFAULT_FIELDS;
- } else if (widget.fields.length > MAX_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_FIELDS);
- }
-
const { data, error } = useWidgetAPI(widget);
if (error) {
diff --git a/src/widgets/mastodon/component.jsx b/src/widgets/mastodon/component.jsx
index dc222bcba..e9bda6488 100644
--- a/src/widgets/mastodon/component.jsx
+++ b/src/widgets/mastodon/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/mealie/component.jsx b/src/widgets/mealie/component.jsx
index 69b2cccf3..795eeeb87 100644
--- a/src/widgets/mealie/component.jsx
+++ b/src/widgets/mealie/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/medusa/component.jsx b/src/widgets/medusa/component.jsx
index fe4c69796..aa46dc99f 100644
--- a/src/widgets/medusa/component.jsx
+++ b/src/widgets/medusa/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/mikrotik/component.jsx b/src/widgets/mikrotik/component.jsx
index 00f2edc92..c8dd141a6 100644
--- a/src/widgets/mikrotik/component.jsx
+++ b/src/widgets/mikrotik/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/minecraft/component.jsx b/src/widgets/minecraft/component.jsx
index 1e6698a63..912febef0 100644
--- a/src/widgets/minecraft/component.jsx
+++ b/src/widgets/minecraft/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/miniflux/component.jsx b/src/widgets/miniflux/component.jsx
index c3dc0954c..965e6f5b0 100644
--- a/src/widgets/miniflux/component.jsx
+++ b/src/widgets/miniflux/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/moonraker/component.jsx b/src/widgets/moonraker/component.jsx
index f2576d78c..04553bda4 100644
--- a/src/widgets/moonraker/component.jsx
+++ b/src/widgets/moonraker/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/mylar/component.jsx b/src/widgets/mylar/component.jsx
index d6175af46..609e99130 100644
--- a/src/widgets/mylar/component.jsx
+++ b/src/widgets/mylar/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/myspeed/component.jsx b/src/widgets/myspeed/component.jsx
index 5b7e225a6..45de95a32 100644
--- a/src/widgets/myspeed/component.jsx
+++ b/src/widgets/myspeed/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/navidrome/component.jsx b/src/widgets/navidrome/component.jsx
index 8427d930f..2c59c7957 100644
--- a/src/widgets/navidrome/component.jsx
+++ b/src/widgets/navidrome/component.jsx
@@ -1,6 +1,6 @@
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function SinglePlayingEntry({ entry }) {
diff --git a/src/widgets/netalertx/component.jsx b/src/widgets/netalertx/component.jsx
index 280425806..f494abd55 100644
--- a/src/widgets/netalertx/component.jsx
+++ b/src/widgets/netalertx/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/netdata/component.jsx b/src/widgets/netdata/component.jsx
index 1e1811a7e..c675d1bf2 100644
--- a/src/widgets/netdata/component.jsx
+++ b/src/widgets/netdata/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/nextcloud/component.jsx b/src/widgets/nextcloud/component.jsx
index e3fb0948f..e30fd893a 100755
--- a/src/widgets/nextcloud/component.jsx
+++ b/src/widgets/nextcloud/component.jsx
@@ -1,8 +1,8 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import { useMemo } from "react";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/nextdns/component.jsx b/src/widgets/nextdns/component.jsx
index 2fd5effc6..22612b197 100644
--- a/src/widgets/nextdns/component.jsx
+++ b/src/widgets/nextdns/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/npm/component.jsx b/src/widgets/npm/component.jsx
index 54e123a8d..032b8ba54 100644
--- a/src/widgets/npm/component.jsx
+++ b/src/widgets/npm/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/ntfy/component.jsx b/src/widgets/ntfy/component.jsx
index 2dcdd2754..239510b20 100644
--- a/src/widgets/ntfy/component.jsx
+++ b/src/widgets/ntfy/component.jsx
@@ -1,8 +1,11 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
+
+const DEFAULT_FIELDS = ["title", "message", "priority", "lastReceived"];
const priorityLabels = {
1: "min",
@@ -20,8 +23,9 @@ function Truncated({ text }) {
);
}
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
const { data: messagesData, error: messagesError } = useWidgetAPI(widget, "messages");
@@ -30,12 +34,6 @@ export default function Component({ service }) {
return ;
}
- if (!widget.fields || widget.fields.length === 0) {
- widget.fields = ["title", "message", "priority", "lastReceived"];
- } else if (widget.fields?.length > 4) {
- widget.fields = widget.fields.slice(0, 4);
- }
-
if (!messagesData) {
return (
diff --git a/src/widgets/nzbget/component.jsx b/src/widgets/nzbget/component.jsx
index 24e480976..3606e0bd8 100644
--- a/src/widgets/nzbget/component.jsx
+++ b/src/widgets/nzbget/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/octoprint/component.jsx b/src/widgets/octoprint/component.jsx
index e6b138091..0844306e7 100644
--- a/src/widgets/octoprint/component.jsx
+++ b/src/widgets/octoprint/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/omada/component.jsx b/src/widgets/omada/component.jsx
index 44ea2a654..c03e93c63 100644
--- a/src/widgets/omada/component.jsx
+++ b/src/widgets/omada/component.jsx
@@ -3,10 +3,14 @@ import { useTranslation } from "next-i18next/pages";
import Block from "../../components/services/widget/block";
import Container from "../../components/services/widget/container";
import useWidgetAPI from "../../utils/proxy/use-widget-api";
+import withWidgetFields from "../../utils/widget-fields";
-export default function Component({ service }) {
+const DEFAULT_FIELDS = ["connectedAp", "activeUser", "alerts", "connectedGateways"];
+
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
const { data: omadaData, error: omadaAPIError } = useWidgetAPI(widget, "info", {
@@ -17,12 +21,6 @@ export default function Component({ service }) {
return ;
}
- if (!widget.fields) {
- widget.fields = ["connectedAp", "activeUser", "alerts", "connectedGateways"];
- } else if (widget.fields?.length > 4) {
- widget.fields = widget.fields.slice(0, 4);
- }
-
if (!omadaData) {
return (
diff --git a/src/widgets/ombi/component.jsx b/src/widgets/ombi/component.jsx
index 859c01aa7..acefcfbf3 100644
--- a/src/widgets/ombi/component.jsx
+++ b/src/widgets/ombi/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/opendtu/component.jsx b/src/widgets/opendtu/component.jsx
index 4ded47896..00e446c62 100644
--- a/src/widgets/opendtu/component.jsx
+++ b/src/widgets/opendtu/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx b/src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx
index 7caf34265..712ea0272 100644
--- a/src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx
+++ b/src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
const downloadReduce = (acc, e) => {
diff --git a/src/widgets/openmediavault/methods/services_get_status.jsx b/src/widgets/openmediavault/methods/services_get_status.jsx
index 7ab0f8c1a..daa18e61c 100644
--- a/src/widgets/openmediavault/methods/services_get_status.jsx
+++ b/src/widgets/openmediavault/methods/services_get_status.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
const isRunningReduce = (acc, e) => {
diff --git a/src/widgets/openmediavault/methods/smart_get_list.jsx b/src/widgets/openmediavault/methods/smart_get_list.jsx
index 4998c02fa..c3cd0a4d3 100644
--- a/src/widgets/openmediavault/methods/smart_get_list.jsx
+++ b/src/widgets/openmediavault/methods/smart_get_list.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
const passedReduce = (acc, e) => {
diff --git a/src/widgets/openwrt/methods/interface.jsx b/src/widgets/openwrt/methods/interface.jsx
index 0e7c773bd..00e456111 100644
--- a/src/widgets/openwrt/methods/interface.jsx
+++ b/src/widgets/openwrt/methods/interface.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/openwrt/methods/system.jsx b/src/widgets/openwrt/methods/system.jsx
index f0bf86106..3d3068886 100644
--- a/src/widgets/openwrt/methods/system.jsx
+++ b/src/widgets/openwrt/methods/system.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/opnsense/component.jsx b/src/widgets/opnsense/component.jsx
index b60af25ca..26c5cbd2a 100644
--- a/src/widgets/opnsense/component.jsx
+++ b/src/widgets/opnsense/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/overseerr/component.jsx b/src/widgets/overseerr/component.jsx
index 47ae32dcc..1961498a9 100644
--- a/src/widgets/overseerr/component.jsx
+++ b/src/widgets/overseerr/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/pangolin/component.jsx b/src/widgets/pangolin/component.jsx
index 3724cf9f5..009fdd8be 100644
--- a/src/widgets/pangolin/component.jsx
+++ b/src/widgets/pangolin/component.jsx
@@ -1,21 +1,17 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-const MAX_ALLOWED_FIELDS = 4;
+const DEFAULT_FIELDS = ["sites", "resources", "targets", "traffic"];
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
- if (!widget.fields) {
- widget.fields = ["sites", "resources", "targets", "traffic"];
- } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
const { data: sitesData, error: sitesError } = useWidgetAPI(widget, "sites");
const { data: resourcesData, error: resourcesError } = useWidgetAPI(widget, "resources");
diff --git a/src/widgets/pangolin/component.test.jsx b/src/widgets/pangolin/component.test.jsx
index 4796b2ee1..873b6b96e 100644
--- a/src/widgets/pangolin/component.test.jsx
+++ b/src/widgets/pangolin/component.test.jsx
@@ -22,7 +22,7 @@ describe("widgets/pangolin/component", () => {
const service = { widget: { type: "pangolin" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["sites", "resources", "targets", "traffic"]);
+ expect(service.widget.fields).toBeUndefined();
// Container filters by widget.fields, so only the default 4 blocks are visible.
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("pangolin.sites")).toBeInTheDocument();
@@ -39,7 +39,7 @@ describe("widgets/pangolin/component", () => {
const service = { widget: { type: "pangolin", fields: ["sites", "resources", "targets", "traffic", "extra"] } };
renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["sites", "resources", "targets", "traffic"]);
+ expect(service.widget.fields).toEqual(["sites", "resources", "targets", "traffic", "extra"]);
});
it("renders computed site/resource/target totals and traffic bytes", () => {
diff --git a/src/widgets/paperlessngx/component.jsx b/src/widgets/paperlessngx/component.jsx
index bafc2e99f..acc2f1e2e 100644
--- a/src/widgets/paperlessngx/component.jsx
+++ b/src/widgets/paperlessngx/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/peanut/component.jsx b/src/widgets/peanut/component.jsx
index e7767249d..8ff6b05b5 100644
--- a/src/widgets/peanut/component.jsx
+++ b/src/widgets/peanut/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
@@ -25,18 +25,12 @@ export default function Component({ service }) {
}
// backwards compatibility with peanut v1
- if ("battery.charge" in upsData) {
- upsData.battery_charge = upsData["battery.charge"];
- }
- if ("ups.load" in upsData) {
- upsData.ups_load = upsData["ups.load"];
- }
- if ("ups.status" in upsData) {
- upsData.ups_status = upsData["ups.status"];
- }
+ const batteryCharge = "battery.charge" in upsData ? upsData["battery.charge"] : upsData.battery_charge;
+ const upsLoad = "ups.load" in upsData ? upsData["ups.load"] : upsData.ups_load;
+ const upsStatus = "ups.status" in upsData ? upsData["ups.status"] : upsData.ups_status;
let status;
- switch (upsData.ups_status) {
+ switch (upsStatus) {
case "OL":
status = t("peanut.online");
break;
@@ -47,21 +41,17 @@ export default function Component({ service }) {
status = t("peanut.low_battery");
break;
default:
- status = upsData.ups_status;
+ status = upsStatus;
}
return (
-
+
);
diff --git a/src/widgets/peanut/component.test.jsx b/src/widgets/peanut/component.test.jsx
index b7a0b1423..466cddf9d 100644
--- a/src/widgets/peanut/component.test.jsx
+++ b/src/widgets/peanut/component.test.jsx
@@ -34,12 +34,13 @@ describe("widgets/peanut/component", () => {
});
it("renders legacy field mapping and status translation", () => {
+ const data = {
+ "battery.charge": 55,
+ "ups.load": 12,
+ "ups.status": "OL",
+ };
useWidgetAPI.mockReturnValue({
- data: {
- "battery.charge": 55,
- "ups.load": 12,
- "ups.status": "OL",
- },
+ data,
error: undefined,
});
@@ -48,5 +49,6 @@ describe("widgets/peanut/component", () => {
expect(screen.getByText("55")).toBeInTheDocument();
expect(screen.getByText("12")).toBeInTheDocument();
expect(screen.getByText("peanut.online")).toBeInTheDocument();
+ expect(data).toEqual({ "battery.charge": 55, "ups.load": 12, "ups.status": "OL" });
});
});
diff --git a/src/widgets/pfsense/component.jsx b/src/widgets/pfsense/component.jsx
index f4ff2792d..662693bc7 100644
--- a/src/widgets/pfsense/component.jsx
+++ b/src/widgets/pfsense/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/photoprism/component.jsx b/src/widgets/photoprism/component.jsx
index 2dc53c58d..70afe3711 100644
--- a/src/widgets/photoprism/component.jsx
+++ b/src/widgets/photoprism/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/pihole/component.jsx b/src/widgets/pihole/component.jsx
index 04b3a64a6..ca74d11e5 100644
--- a/src/widgets/pihole/component.jsx
+++ b/src/widgets/pihole/component.jsx
@@ -1,12 +1,16 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const DEFAULT_FIELDS = ["queries", "blocked", "gravity"];
+
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
const { data: piholeData, error: piholeError } = useWidgetAPI(widget);
@@ -15,10 +19,6 @@ export default function Component({ service }) {
return ;
}
- if (!widget.fields) {
- widget.fields = ["queries", "blocked", "gravity"];
- }
-
if (!piholeData) {
return (
diff --git a/src/widgets/plantit/component.jsx b/src/widgets/plantit/component.jsx
index d4c136f06..c2a4bda3c 100644
--- a/src/widgets/plantit/component.jsx
+++ b/src/widgets/plantit/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/plex/component.jsx b/src/widgets/plex/component.jsx
index 96ad70328..63248012e 100644
--- a/src/widgets/plex/component.jsx
+++ b/src/widgets/plex/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/portainer/component.jsx b/src/widgets/portainer/component.jsx
index c0c212cfd..50b3fa2be 100644
--- a/src/widgets/portainer/component.jsx
+++ b/src/widgets/portainer/component.jsx
@@ -1,15 +1,16 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const DOCKER_FIELDS = ["running", "stopped", "total"];
+const KUBERNETES_FIELDS = ["applications", "services", "namespaces"];
+
+export default function Component({ service: configuredService }) {
+ const defaultFields = configuredService.widget.kubernetes ? KUBERNETES_FIELDS : DOCKER_FIELDS;
+ const service = withWidgetFields(configuredService, defaultFields);
const { widget } = service;
- if (!widget.fields) {
- widget.fields = widget.kubernetes ? ["applications", "services", "namespaces"] : ["running", "stopped", "total"];
- }
-
const { data: containersCount, error: containersError } = useWidgetAPI(
widget,
widget.kubernetes ? "" : "docker/containers",
diff --git a/src/widgets/portainer/component.test.jsx b/src/widgets/portainer/component.test.jsx
index b87845cde..e513250c3 100644
--- a/src/widgets/portainer/component.test.jsx
+++ b/src/widgets/portainer/component.test.jsx
@@ -22,7 +22,7 @@ describe("widgets/portainer/component", () => {
const service = { widget: { type: "portainer", kubernetes: false } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["running", "stopped", "total"]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("portainer.running")).toBeInTheDocument();
expect(screen.getByText("portainer.stopped")).toBeInTheDocument();
@@ -50,7 +50,7 @@ describe("widgets/portainer/component", () => {
const service = { widget: { type: "portainer", kubernetes: true } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["applications", "services", "namespaces"]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("portainer.applications")).toBeInTheDocument();
expect(screen.getByText("portainer.services")).toBeInTheDocument();
diff --git a/src/widgets/prometheus/component.jsx b/src/widgets/prometheus/component.jsx
index 027d16785..36f68b837 100644
--- a/src/widgets/prometheus/component.jsx
+++ b/src/widgets/prometheus/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/prometheusmetric/component.jsx b/src/widgets/prometheusmetric/component.jsx
index 05978db41..e0c7b078f 100644
--- a/src/widgets/prometheusmetric/component.jsx
+++ b/src/widgets/prometheusmetric/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function formatValue(t, metric, rawValue) {
@@ -54,23 +54,19 @@ export default function Component({ service }) {
const { metrics = [], refreshInterval = 10000 } = widget;
- let prometheusmetricError;
+ const metricResults = metrics.slice(0, 4).map((metric) => {
+ // disable the rule that hooks should not be called from a callback,
+ // because we don't need a strong guarantee of hook execution order here.
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { data, error } = useWidgetAPI(widget, "query", {
+ query: metric.query,
+ refreshInterval: Math.max(1000, metric.refreshInterval ?? refreshInterval),
+ });
+ return { key: metric.key ?? metric.label, data, error };
+ });
- const prometheusmetricData = new Map(
- metrics.slice(0, 4).map((metric) => {
- // disable the rule that hooks should not be called from a callback,
- // because we don't need a strong guarantee of hook execution order here.
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const { data: resultData, error: resultError } = useWidgetAPI(widget, "query", {
- query: metric.query,
- refreshInterval: Math.max(1000, metric.refreshInterval ?? refreshInterval),
- });
- if (resultError) {
- prometheusmetricError = resultError;
- }
- return [metric.key ?? metric.label, resultData];
- }),
- );
+ const prometheusmetricError = metricResults.find(({ error }) => error)?.error;
+ const prometheusmetricData = new Map(metricResults.map(({ key, data }) => [key, data]));
if (prometheusmetricError) {
return ;
diff --git a/src/widgets/prowlarr/component.jsx b/src/widgets/prowlarr/component.jsx
index 9a9a8e66b..2d3d8e0dc 100644
--- a/src/widgets/prowlarr/component.jsx
+++ b/src/widgets/prowlarr/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/proxmox/component.jsx b/src/widgets/proxmox/component.jsx
index e238fadac..54333c0af 100644
--- a/src/widgets/proxmox/component.jsx
+++ b/src/widgets/proxmox/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function calcRunning(total, current) {
diff --git a/src/widgets/proxmoxbackupserver/component.jsx b/src/widgets/proxmoxbackupserver/component.jsx
index 0b4d8f315..387a74932 100644
--- a/src/widgets/proxmoxbackupserver/component.jsx
+++ b/src/widgets/proxmoxbackupserver/component.jsx
@@ -1,21 +1,29 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+import useCurrentTime from "utils/hooks/use-current-time";
import useWidgetAPI from "utils/proxy/use-widget-api";
+const HOUR = 60 * 60 * 1000;
+
export default function Component({ service }) {
const { t } = useTranslation();
+ // hourly is plenty of precision for a 24 hour window, and `since` is part of the request key
+ const currentTime = useCurrentTime(HOUR);
const { widget } = service;
const taskQueryParams = {
errors: true,
limit: 100,
- since: Math.floor(Date.now() / 1000) - 24 * 60 * 60,
+ // omitted until mounted, so the request key doesn't churn during hydration
+ ...(currentTime !== null && { since: Math.floor(currentTime / 1000) - 24 * 60 * 60 }),
};
const { data: datastoreData, error: datastoreError } = useWidgetAPI(widget, "status/datastore-usage");
- const { data: tasksData, error: tasksError } = useWidgetAPI(widget, "nodes/localhost/tasks", taskQueryParams);
+ const { data: tasksData, error: tasksError } = useWidgetAPI(widget, "nodes/localhost/tasks", taskQueryParams, {
+ keepPreviousData: true, // `since` rotates the key, don't blank the whole widget while it refetches
+ });
const { data: hostData, error: hostError } = useWidgetAPI(widget, "nodes/localhost/status");
if (datastoreError || tasksError || hostError) {
diff --git a/src/widgets/proxmoxbackupserver/component.test.jsx b/src/widgets/proxmoxbackupserver/component.test.jsx
index 3129ae996..19c691848 100644
--- a/src/widgets/proxmoxbackupserver/component.test.jsx
+++ b/src/widgets/proxmoxbackupserver/component.test.jsx
@@ -15,16 +15,20 @@ vi.mock("utils/proxy/use-widget-api", () => ({
import Component from "./component";
+const empty = { data: undefined, error: undefined };
+
+// keyed by endpoint rather than call order, since the component renders more than once
+function mockEndpoints(byEndpoint = {}) {
+ useWidgetAPI.mockImplementation((widget, endpoint) => byEndpoint[endpoint] ?? empty);
+}
+
describe("widgets/proxmoxbackupserver/component", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("renders placeholders while loading", () => {
- useWidgetAPI
- .mockReturnValueOnce({ data: undefined, error: undefined }) // datastore
- .mockReturnValueOnce({ data: undefined, error: undefined }) // tasks
- .mockReturnValueOnce({ data: undefined, error: undefined }); // host
+ mockEndpoints();
const { container } = renderWithProviders(, {
settings: { hideErrors: false },
@@ -38,10 +42,7 @@ describe("widgets/proxmoxbackupserver/component", () => {
});
it("renders error UI when any endpoint errors", () => {
- useWidgetAPI
- .mockReturnValueOnce({ data: undefined, error: undefined })
- .mockReturnValueOnce({ data: undefined, error: { message: "nope" } })
- .mockReturnValueOnce({ data: undefined, error: undefined });
+ mockEndpoints({ "nodes/localhost/tasks": { data: undefined, error: { message: "nope" } } });
renderWithProviders(, {
settings: { hideErrors: false },
@@ -51,8 +52,8 @@ describe("widgets/proxmoxbackupserver/component", () => {
});
it("renders computed values and caps failed tasks at 99+", () => {
- useWidgetAPI
- .mockReturnValueOnce({
+ mockEndpoints({
+ "status/datastore-usage": {
data: {
data: [
{ store: "ds1", used: 50, total: 100 },
@@ -60,9 +61,10 @@ describe("widgets/proxmoxbackupserver/component", () => {
],
},
error: undefined,
- })
- .mockReturnValueOnce({ data: { total: 1000 }, error: undefined })
- .mockReturnValueOnce({ data: { data: { cpu: 0.2, memory: { used: 1, total: 4 } } }, error: undefined });
+ },
+ "nodes/localhost/tasks": { data: { total: 1000 }, error: undefined },
+ "nodes/localhost/status": { data: { data: { cpu: 0.2, memory: { used: 1, total: 4 } } }, error: undefined },
+ });
renderWithProviders(, {
settings: { hideErrors: false },
@@ -78,19 +80,21 @@ describe("widgets/proxmoxbackupserver/component", () => {
it("requests failed tasks with a 24 hour since filter in epoch seconds", () => {
vi.spyOn(Date, "now").mockReturnValue(1_776_519_498_000);
- useWidgetAPI
- .mockReturnValueOnce({ data: undefined, error: undefined })
- .mockReturnValueOnce({ data: undefined, error: undefined })
- .mockReturnValueOnce({ data: undefined, error: undefined });
+ mockEndpoints();
renderWithProviders(, {
settings: { hideErrors: false },
});
- expect(useWidgetAPI).toHaveBeenNthCalledWith(2, { type: "proxmoxbackupserver" }, "nodes/localhost/tasks", {
- errors: true,
- limit: 100,
- since: 1_776_433_098,
- });
+ expect(useWidgetAPI).toHaveBeenCalledWith(
+ { type: "proxmoxbackupserver" },
+ "nodes/localhost/tasks",
+ {
+ errors: true,
+ limit: 100,
+ since: 1_776_433_098,
+ },
+ { keepPreviousData: true },
+ );
});
});
diff --git a/src/widgets/proxmoxvm/component.jsx b/src/widgets/proxmoxvm/component.jsx
index ab50ed672..3722a7d14 100644
--- a/src/widgets/proxmoxvm/component.jsx
+++ b/src/widgets/proxmoxvm/component.jsx
@@ -1,8 +1,9 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import useSWR from "swr";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+
export default function ProxmoxVM({ service }) {
const { t } = useTranslation();
diff --git a/src/widgets/pterodactyl/component.jsx b/src/widgets/pterodactyl/component.jsx
index 9a702eeff..1e88098f1 100644
--- a/src/widgets/pterodactyl/component.jsx
+++ b/src/widgets/pterodactyl/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/pulse/component.jsx b/src/widgets/pulse/component.jsx
index 5ad757866..e4442c5e1 100644
--- a/src/widgets/pulse/component.jsx
+++ b/src/widgets/pulse/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
const ACTIVE_STATUSES = ["online", "running"];
diff --git a/src/widgets/pyload/component.jsx b/src/widgets/pyload/component.jsx
index b2729be2f..1842a0772 100644
--- a/src/widgets/pyload/component.jsx
+++ b/src/widgets/pyload/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/qbittorrent/component.jsx b/src/widgets/qbittorrent/component.jsx
index 97b393278..20e000f4c 100644
--- a/src/widgets/qbittorrent/component.jsx
+++ b/src/widgets/qbittorrent/component.jsx
@@ -1,9 +1,9 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import QueueEntry from "../../components/widgets/queue/queueEntry";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/qnap/component.jsx b/src/widgets/qnap/component.jsx
index 7ac21f256..cd598bc27 100644
--- a/src/widgets/qnap/component.jsx
+++ b/src/widgets/qnap/component.jsx
@@ -1,9 +1,9 @@
/* eslint no-underscore-dangle: ["error", { "allow": ["_text", "_cdata"] }] */
+import { useTranslation } from "next-i18next/pages";
+
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-import { useTranslation } from "next-i18next/pages";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/radarr/component.jsx b/src/widgets/radarr/component.jsx
index 4f6f12480..c6e215ba2 100644
--- a/src/widgets/radarr/component.jsx
+++ b/src/widgets/radarr/component.jsx
@@ -1,9 +1,9 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import QueueEntry from "../../components/widgets/queue/queueEntry";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function getProgress(sizeLeft, size) {
diff --git a/src/widgets/readarr/component.jsx b/src/widgets/readarr/component.jsx
index 53bd05655..f41e3304b 100644
--- a/src/widgets/readarr/component.jsx
+++ b/src/widgets/readarr/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/romm/component.jsx b/src/widgets/romm/component.jsx
index 222f8d4ae..12fe390ce 100644
--- a/src/widgets/romm/component.jsx
+++ b/src/widgets/romm/component.jsx
@@ -1,13 +1,14 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
const ROMM_DEFAULT_FIELDS = ["platforms", "totalRoms", "saves", "states"];
-const MAX_ALLOWED_FIELDS = 4;
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
+ const service = withWidgetFields(configuredService, ROMM_DEFAULT_FIELDS);
const { widget } = service;
const { t } = useTranslation();
const { data: response, error: responseError } = useWidgetAPI(widget, "statistics");
@@ -16,12 +17,6 @@ export default function Component({ service }) {
return ;
}
- if (!widget.fields?.length > 0) {
- widget.fields = ROMM_DEFAULT_FIELDS;
- } else if (widget.fields.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
if (!response) {
return (
diff --git a/src/widgets/romm/component.test.jsx b/src/widgets/romm/component.test.jsx
index 28384f8a1..75086dcb7 100644
--- a/src/widgets/romm/component.test.jsx
+++ b/src/widgets/romm/component.test.jsx
@@ -22,7 +22,7 @@ describe("widgets/romm/component", () => {
const service = { widget: { type: "romm" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["platforms", "totalRoms", "saves", "states"]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("romm.platforms")).toBeInTheDocument();
expect(screen.getByText("romm.totalRoms")).toBeInTheDocument();
@@ -38,7 +38,7 @@ describe("widgets/romm/component", () => {
const service = { widget: { type: "romm", fields: ["platforms", "totalRoms", "saves", "states", "screenshots"] } };
renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["platforms", "totalRoms", "saves", "states"]);
+ expect(service.widget.fields).toEqual(["platforms", "totalRoms", "saves", "states", "screenshots"]);
});
it("renders values when loaded (and includes additional fields when explicitly selected)", () => {
diff --git a/src/widgets/rutorrent/component.jsx b/src/widgets/rutorrent/component.jsx
index d8dd9c0ba..9012f72b1 100644
--- a/src/widgets/rutorrent/component.jsx
+++ b/src/widgets/rutorrent/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/sabnzbd/component.jsx b/src/widgets/sabnzbd/component.jsx
index b5f9a2dfe..9f7ab6f58 100644
--- a/src/widgets/sabnzbd/component.jsx
+++ b/src/widgets/sabnzbd/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function fromUnits(value) {
diff --git a/src/widgets/scrutiny/component.jsx b/src/widgets/scrutiny/component.jsx
index dd870516a..443edad39 100644
--- a/src/widgets/scrutiny/component.jsx
+++ b/src/widgets/scrutiny/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
// @see https://github.com/AnalogJ/scrutiny/blob/d8d56f77f9e868127c4849dac74d65512db658e8/webapp/frontend/src/app/shared/device-status.pipe.ts
diff --git a/src/widgets/seerr/component.jsx b/src/widgets/seerr/component.jsx
index 95d72dc8a..2acae2031 100644
--- a/src/widgets/seerr/component.jsx
+++ b/src/widgets/seerr/component.jsx
@@ -1,14 +1,13 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
export const seerrDefaultFields = ["pending", "approved", "completed"];
-const MAX_ALLOWED_FIELDS = 4;
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
+ const service = withWidgetFields(configuredService, seerrDefaultFields);
const { widget } = service;
- widget.fields = widget?.fields?.length ? widget.fields.slice(0, MAX_ALLOWED_FIELDS) : seerrDefaultFields;
const isIssueEnabled = widget.fields.includes("issues");
const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count");
@@ -30,16 +29,20 @@ export default function Component({ service }) {
);
}
- if (
- statsData.completed === undefined &&
- (widget.fields.includes("completed") || widget.fields.includes("available"))
- ) {
- // Fallback to "available" if "completed" requested but not available
- widget.fields = widget.fields.map((field) => (field === "completed" ? "available" : field));
+ // Older Seerr versions expose "available" instead of "completed".
+ let renderedService = service;
+ if (statsData.completed === undefined && service.widget.fields.includes("completed")) {
+ renderedService = {
+ ...service,
+ widget: {
+ ...service.widget,
+ fields: service.widget.fields.map((field) => (field === "completed" ? "available" : field)),
+ },
+ };
}
return (
-
+
diff --git a/src/widgets/seerr/component.test.jsx b/src/widgets/seerr/component.test.jsx
index a1c51f5c0..755971b1c 100644
--- a/src/widgets/seerr/component.test.jsx
+++ b/src/widgets/seerr/component.test.jsx
@@ -8,7 +8,7 @@ import { renderWithProviders } from "test-utils/render-with-providers";
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
-import Component, { seerrDefaultFields } from "./component";
+import Component from "./component";
describe("widgets/seerr/component", () => {
beforeEach(() => {
@@ -23,7 +23,7 @@ describe("widgets/seerr/component", () => {
const service = { widget: { type: "seerr", url: "http://x" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(seerrDefaultFields);
+ expect(service.widget.fields).toBeUndefined();
expect(useWidgetAPI.mock.calls[1][1]).toBe("");
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("seerr.pending")).toBeInTheDocument();
@@ -42,7 +42,7 @@ describe("widgets/seerr/component", () => {
const service = { widget: { type: "jellyseerr", url: "http://x" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(seerrDefaultFields);
+ expect(service.widget.fields).toBeUndefined();
expect(useWidgetAPI.mock.calls[1][1]).toBe("");
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("seerr.pending")).toBeInTheDocument();
@@ -58,7 +58,7 @@ describe("widgets/seerr/component", () => {
const service = { widget: { type: "overseerr", url: "http://x" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(seerrDefaultFields);
+ expect(service.widget.fields).toBeUndefined();
expect(useWidgetAPI.mock.calls[1][1]).toBe("");
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("seerr.pending")).toBeInTheDocument();
@@ -110,7 +110,7 @@ describe("widgets/seerr/component", () => {
renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["pending", "approved", "available"]);
+ expect(service.widget.fields).toEqual(["pending", "approved", "completed"]);
expect(screen.getByText("3")).toBeInTheDocument();
expect(screen.queryByText("seerr.completed")).toBeNull();
});
diff --git a/src/widgets/slskd/component.jsx b/src/widgets/slskd/component.jsx
index 1d07b3f98..7f9f77448 100644
--- a/src/widgets/slskd/component.jsx
+++ b/src/widgets/slskd/component.jsx
@@ -1,14 +1,15 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
const slskdDefaultFields = ["slskStatus", "downloads", "uploads", "sharedFiles"];
-const MAX_ALLOWED_FIELDS = 4;
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, slskdDefaultFields);
const { widget } = service;
const { data: appData, error: appError } = useWidgetAPI(widget, "application");
@@ -19,12 +20,6 @@ export default function Component({ service }) {
return ;
}
- if (!widget.fields || widget.fields.length === 0) {
- widget.fields = slskdDefaultFields;
- } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
if (!appData || !downData || !upData) {
return (
diff --git a/src/widgets/slskd/component.test.jsx b/src/widgets/slskd/component.test.jsx
index 4228430d1..d050dc939 100644
--- a/src/widgets/slskd/component.test.jsx
+++ b/src/widgets/slskd/component.test.jsx
@@ -22,7 +22,7 @@ describe("widgets/slskd/component", () => {
const service = { widget: { type: "slskd" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["slskStatus", "downloads", "uploads", "sharedFiles"]);
+ expect(service.widget.fields).toBeUndefined();
// Container filters children by widget.fields.
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("slskd.slskStatus")).toBeInTheDocument();
@@ -38,7 +38,7 @@ describe("widgets/slskd/component", () => {
const service = { widget: { type: "slskd", fields: ["a", "b", "c", "d", "e"] } };
renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["a", "b", "c", "d"]);
+ expect(service.widget.fields).toEqual(["a", "b", "c", "d", "e"]);
});
it("renders status and counts when loaded", () => {
diff --git a/src/widgets/sonarr/component.jsx b/src/widgets/sonarr/component.jsx
index fc3f9db7a..0fdb6e615 100644
--- a/src/widgets/sonarr/component.jsx
+++ b/src/widgets/sonarr/component.jsx
@@ -1,9 +1,9 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import QueueEntry from "../../components/widgets/queue/queueEntry";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function getProgress(sizeLeft, size) {
diff --git a/src/widgets/sparkyfitness/component.jsx b/src/widgets/sparkyfitness/component.jsx
index e156ef619..082d340a7 100644
--- a/src/widgets/sparkyfitness/component.jsx
+++ b/src/widgets/sparkyfitness/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/speedtest/component.jsx b/src/widgets/speedtest/component.jsx
index 67336d642..db2399669 100644
--- a/src/widgets/speedtest/component.jsx
+++ b/src/widgets/speedtest/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/spoolman/component.jsx b/src/widgets/spoolman/component.jsx
index a30b67f7e..9adad3ee2 100644
--- a/src/widgets/spoolman/component.jsx
+++ b/src/widgets/spoolman/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/sportarr/component.jsx b/src/widgets/sportarr/component.jsx
index 83b906ae4..43c472ba1 100644
--- a/src/widgets/sportarr/component.jsx
+++ b/src/widgets/sportarr/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/stash/component.jsx b/src/widgets/stash/component.jsx
index 485e2401e..ee21ca1ed 100644
--- a/src/widgets/stash/component.jsx
+++ b/src/widgets/stash/component.jsx
@@ -1,26 +1,30 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import { useEffect, useState } from "react";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { formatProxyUrl } from "utils/proxy/api-helpers";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const DEFAULT_FIELDS = ["scenes", "images"];
+
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
- const { widget } = service;
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
+ const requestWidget = configuredService.widget;
const [stats, setStats] = useState(null);
useEffect(() => {
async function fetchStats() {
- const url = formatProxyUrl(widget, "stats");
+ const url = formatProxyUrl(requestWidget, "stats");
const res = await fetch(url, { method: "POST" });
setStats(await res.json());
}
if (!stats) {
fetchStats();
}
- }, [widget, stats]);
+ }, [requestWidget, stats]);
if (!stats) {
return (
@@ -31,16 +35,6 @@ export default function Component({ service }) {
);
}
- // Provide a default if not set in the config
- if (!widget.fields) {
- widget.fields = ["scenes", "images"];
- }
-
- // Limit to a maximum of 4 at a time
- if (widget.fields.length > 4) {
- widget.fields = widget.fields.slice(0, 4);
- }
-
return (
diff --git a/src/widgets/stocks/component.jsx b/src/widgets/stocks/component.jsx
index 632fcee50..6c608d2fa 100644
--- a/src/widgets/stocks/component.jsx
+++ b/src/widgets/stocks/component.jsx
@@ -1,8 +1,8 @@
import classNames from "classnames";
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function MarketStatus({ service }) {
diff --git a/src/widgets/strelaysrv/component.jsx b/src/widgets/strelaysrv/component.jsx
index 5412a27c5..21cc83633 100644
--- a/src/widgets/strelaysrv/component.jsx
+++ b/src/widgets/strelaysrv/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/suwayomi/component.jsx b/src/widgets/suwayomi/component.jsx
index f321d46d7..dd4623604 100644
--- a/src/widgets/suwayomi/component.jsx
+++ b/src/widgets/suwayomi/component.jsx
@@ -1,12 +1,16 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const DEFAULT_FIELDS = ["download", "nondownload", "read", "unread"];
+
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
const { data: suwayomiData, error: suwayomiError } = useWidgetAPI(widget);
@@ -16,11 +20,6 @@ export default function Component({ service }) {
}
if (!suwayomiData) {
- if (!widget.fields || widget.fields.length === 0) {
- widget.fields = ["download", "nondownload", "read", "unread"];
- } else if (widget.fields.length > 4) {
- widget.fields = widget.fields.slice(0, 4);
- }
return (
{widget.fields.map((field) => (
diff --git a/src/widgets/suwayomi/component.test.jsx b/src/widgets/suwayomi/component.test.jsx
index a3a779d85..152efb1a0 100644
--- a/src/widgets/suwayomi/component.test.jsx
+++ b/src/widgets/suwayomi/component.test.jsx
@@ -22,7 +22,7 @@ describe("widgets/suwayomi/component", () => {
const service = { widget: { type: "suwayomi" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["download", "nondownload", "read", "unread"]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("suwayomi.download")).toBeInTheDocument();
expect(screen.getByText("suwayomi.nondownload")).toBeInTheDocument();
diff --git a/src/widgets/swagdashboard/component.jsx b/src/widgets/swagdashboard/component.jsx
index 4220e3c8e..f08a6862b 100644
--- a/src/widgets/swagdashboard/component.jsx
+++ b/src/widgets/swagdashboard/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/syncthing/component.jsx b/src/widgets/syncthing/component.jsx
index e83c8a246..93ba002e1 100644
--- a/src/widgets/syncthing/component.jsx
+++ b/src/widgets/syncthing/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/tailscale/component.jsx b/src/widgets/tailscale/component.jsx
index dfba539fc..7be822fbd 100644
--- a/src/widgets/tailscale/component.jsx
+++ b/src/widgets/tailscale/component.jsx
@@ -1,12 +1,16 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const DEFAULT_FIELDS = ["address", "last_seen", "expires"];
+
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
const { data: tailscaleData, error: tailscaleError } = useWidgetAPI(widget, "device");
@@ -25,13 +29,6 @@ export default function Component({ service }) {
);
}
- const MAX_ALLOWED_FIELDS = 4;
- if (widget.fields?.length == 0 || !widget.fields) {
- widget.fields = ["address", "last_seen", "expires"];
- } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
const {
addresses: [address],
keyExpiryDisabled,
diff --git a/src/widgets/tandoor/component.jsx b/src/widgets/tandoor/component.jsx
index d7f8ead72..5990b2153 100644
--- a/src/widgets/tandoor/component.jsx
+++ b/src/widgets/tandoor/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/tautulli/component.jsx b/src/widgets/tautulli/component.jsx
index ea4edf7d4..bb913fc95 100644
--- a/src/widgets/tautulli/component.jsx
+++ b/src/widgets/tautulli/component.jsx
@@ -1,8 +1,8 @@
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import { BsCpu, BsFillCpuFill, BsFillPlayFill, BsPauseFill } from "react-icons/bs";
import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function millisecondsToTime(milliseconds) {
@@ -166,7 +166,7 @@ export default function Component({ service }) {
);
}
- const playing = activityData.response.data.sessions.sort((a, b) => {
+ const playing = [...activityData.response.data.sessions].sort((a, b) => {
if (a.view_offset > b.view_offset) {
return 1;
}
diff --git a/src/widgets/tdarr/component.jsx b/src/widgets/tdarr/component.jsx
index 3d362f452..4730fd3f2 100644
--- a/src/widgets/tdarr/component.jsx
+++ b/src/widgets/tdarr/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/technitium/component.jsx b/src/widgets/technitium/component.jsx
index d0307b0ba..cb9007984 100644
--- a/src/widgets/technitium/component.jsx
+++ b/src/widgets/technitium/component.jsx
@@ -1,16 +1,16 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
-
-const MAX_ALLOWED_FIELDS = 4;
+import withWidgetFields from "utils/widget-fields";
export const technitiumDefaultFields = ["totalQueries", "totalAuthoritative", "totalCached", "totalServerFailure"];
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, technitiumDefaultFields);
const { widget } = service;
const params = {
@@ -20,16 +20,6 @@ export default function Component({ service }) {
const { data: statsData, error: statsError } = useWidgetAPI(widget, "stats", params);
- // Default fields
- if (!widget.fields?.length > 0) {
- widget.fields = technitiumDefaultFields;
- }
-
- // Limits max number of displayed fields
- if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
if (statsError) {
return ;
}
diff --git a/src/widgets/technitium/component.test.jsx b/src/widgets/technitium/component.test.jsx
index 7888d687d..533b466a3 100644
--- a/src/widgets/technitium/component.test.jsx
+++ b/src/widgets/technitium/component.test.jsx
@@ -9,7 +9,7 @@ import { expectBlockValue, findServiceBlockByLabel } from "test-utils/widget-ass
const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() }));
vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI }));
-import Component, { technitiumDefaultFields } from "./component";
+import Component from "./component";
describe("widgets/technitium/component", () => {
beforeEach(() => {
@@ -22,7 +22,7 @@ describe("widgets/technitium/component", () => {
const service = { widget: { type: "technitium" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(technitiumDefaultFields);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("technitium.totalQueries")).toBeInTheDocument();
expect(screen.getByText("technitium.totalAuthoritative")).toBeInTheDocument();
diff --git a/src/widgets/tracearr/component.jsx b/src/widgets/tracearr/component.jsx
index e2586d223..d3ff06c23 100644
--- a/src/widgets/tracearr/component.jsx
+++ b/src/widgets/tracearr/component.jsx
@@ -1,9 +1,9 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import { BsCpu, BsFillCpuFill, BsFillPlayFill, BsPauseFill } from "react-icons/bs";
import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function millisecondsToTime(milliseconds) {
@@ -232,7 +232,7 @@ export default function Component({ service }) {
);
}
- const playing = activityData.data.sort((a, b) => a.progressMs - b.progressMs);
+ const playing = [...activityData.data].sort((a, b) => a.progressMs - b.progressMs);
const { summary } = activityData;
if (view === "summary") {
diff --git a/src/widgets/traefik/component.jsx b/src/widgets/traefik/component.jsx
index e4b3b46ba..6b45a4772 100644
--- a/src/widgets/traefik/component.jsx
+++ b/src/widgets/traefik/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/transmission/component.jsx b/src/widgets/transmission/component.jsx
index 0650e41d0..65ae33e4e 100644
--- a/src/widgets/transmission/component.jsx
+++ b/src/widgets/transmission/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/trilium/component.jsx b/src/widgets/trilium/component.jsx
index 7b83a6ab3..528941efd 100644
--- a/src/widgets/trilium/component.jsx
+++ b/src/widgets/trilium/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/truenas/component.jsx b/src/widgets/truenas/component.jsx
index cb5891267..f712b4b03 100644
--- a/src/widgets/truenas/component.jsx
+++ b/src/widgets/truenas/component.jsx
@@ -1,9 +1,9 @@
+import { useTranslation } from "next-i18next/pages";
+
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-import { useTranslation } from "next-i18next/pages";
-import Pool from "widgets/truenas/pool";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
+import Pool from "widgets/truenas/pool";
export default function Component({ service }) {
const { t } = useTranslation();
diff --git a/src/widgets/tubearchivist/component.jsx b/src/widgets/tubearchivist/component.jsx
index 524dd83a3..5571b88d1 100644
--- a/src/widgets/tubearchivist/component.jsx
+++ b/src/widgets/tubearchivist/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/unifi/component.jsx b/src/widgets/unifi/component.jsx
index 11770fd36..d273b416f 100644
--- a/src/widgets/unifi/component.jsx
+++ b/src/widgets/unifi/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/unifi_drive/component.jsx b/src/widgets/unifi_drive/component.jsx
index 69a10d286..f211e5951 100644
--- a/src/widgets/unifi_drive/component.jsx
+++ b/src/widgets/unifi_drive/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/unmanic/component.jsx b/src/widgets/unmanic/component.jsx
index 12069e523..9cadf60c5 100644
--- a/src/widgets/unmanic/component.jsx
+++ b/src/widgets/unmanic/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useEffect, useState } from "react";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { formatProxyUrl } from "utils/proxy/api-helpers";
import useWidgetAPI from "utils/proxy/use-widget-api";
diff --git a/src/widgets/unraid/component.jsx b/src/widgets/unraid/component.jsx
index 99ec6244c..e64772c11 100644
--- a/src/widgets/unraid/component.jsx
+++ b/src/widgets/unraid/component.jsx
@@ -1,11 +1,11 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
const UNRAID_DEFAULT_FIELDS = ["status", "cpu", "memoryPercent", "notifications"];
-const MAX_ALLOWED_FIELDS = 4;
const POOLS = ["pool1", "pool2", "pool3", "pool4"];
const POOL_FIELDS = [
@@ -14,8 +14,9 @@ const POOL_FIELDS = [
{ param: "UsedPercent", label: "poolUsed", valueKey: "fsUsedPercent", valueType: "common.percent" },
];
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, UNRAID_DEFAULT_FIELDS);
const { widget } = service;
const { data, error } = useWidgetAPI(widget);
@@ -24,12 +25,6 @@ export default function Component({ service }) {
return ;
}
- if (!widget.fields?.length) {
- widget.fields = UNRAID_DEFAULT_FIELDS;
- } else if (widget.fields.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
if (!data) {
return (
diff --git a/src/widgets/unraid/component.test.jsx b/src/widgets/unraid/component.test.jsx
index e6b82743d..e5d80b279 100644
--- a/src/widgets/unraid/component.test.jsx
+++ b/src/widgets/unraid/component.test.jsx
@@ -20,14 +20,17 @@ describe("widgets/unraid/component", () => {
vi.clearAllMocks();
});
- it("defaults widget.fields and filters down to 4 visible blocks while loading", () => {
+ it("defaults fields and filters down to 4 visible blocks while loading", () => {
useWidgetAPI.mockReturnValue({ data: undefined, error: undefined });
const service = { widget: { type: "unraid" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- // Component sets default fields
- expect(service.widget.fields).toEqual(["status", "cpu", "memoryPercent", "notifications"]);
+ expect(service.widget.fields).toBeUndefined();
+ expect(useWidgetAPI).toHaveBeenCalledWith({
+ type: "unraid",
+ fields: ["status", "cpu", "memoryPercent", "notifications"],
+ });
// Container filters the many placeholder Blocks down to the selected fields.
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
diff --git a/src/widgets/uptimekuma/component.jsx b/src/widgets/uptimekuma/component.jsx
index 5c35bb539..6d144a7b8 100644
--- a/src/widgets/uptimekuma/component.jsx
+++ b/src/widgets/uptimekuma/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/uptimerobot/component.jsx b/src/widgets/uptimerobot/component.jsx
index 2ed5c460c..071d0287f 100644
--- a/src/widgets/uptimerobot/component.jsx
+++ b/src/widgets/uptimerobot/component.jsx
@@ -1,8 +1,8 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import { useEffect, useState } from "react";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import { formatProxyUrl } from "utils/proxy/api-helpers";
export default function Component({ service }) {
diff --git a/src/widgets/urbackup/component.jsx b/src/widgets/urbackup/component.jsx
index 2d6729953..09c3198e4 100644
--- a/src/widgets/urbackup/component.jsx
+++ b/src/widgets/urbackup/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
const Status = Object.freeze({
diff --git a/src/widgets/vikunja/component.jsx b/src/widgets/vikunja/component.jsx
index 186c80905..b35739cd3 100644
--- a/src/widgets/vikunja/component.jsx
+++ b/src/widgets/vikunja/component.jsx
@@ -1,11 +1,15 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
+import useCurrentTime from "utils/hooks/use-current-time";
import useWidgetAPI from "utils/proxy/use-widget-api";
+const WEEK = 7 * 24 * 60 * 60 * 1000;
+
export default function Component({ service }) {
const { t } = useTranslation();
+ const currentTime = useCurrentTime();
const { widget } = service;
const version = widget.version ?? 1;
@@ -32,10 +36,10 @@ export default function Component({ service }) {
const projects = projectsData.filter((project) => project.id > 0); // saved filters have id < 0
- const oneWeekFromNow = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
+ const oneWeekFromNow = new Date(currentTime + WEEK);
const tasksWithDueDate = tasksData.filter((task) => !task.dueDateIsDefault);
const tasks7d = tasksWithDueDate.filter((task) => new Date(task.dueDate) <= oneWeekFromNow);
- const tasksOverdue = tasksWithDueDate.filter((task) => new Date(task.dueDate) <= new Date(Date.now()));
+ const tasksOverdue = tasksWithDueDate.filter((task) => new Date(task.dueDate) <= new Date(currentTime));
const tasksInProgress = tasksData.filter((task) => task.inProgress);
return (
diff --git a/src/widgets/wallos/component.jsx b/src/widgets/wallos/component.jsx
index 93673a630..08dda6ba8 100644
--- a/src/widgets/wallos/component.jsx
+++ b/src/widgets/wallos/component.jsx
@@ -1,10 +1,11 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-const MAX_ALLOWED_FIELDS = 4;
+const DEFAULT_FIELDS = ["activeSubscriptions", "nextRenewingSubscription", "thisMonthlyCost", "nextMonthlyCost"];
const todayDate = new Date();
function toApiMonthYear(offset = 0) {
@@ -16,16 +17,11 @@ function toApiMonthYear(offset = 0) {
};
}
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
- if (!widget.fields) {
- widget.fields = ["activeSubscriptions", "nextRenewingSubscription", "thisMonthlyCost", "nextMonthlyCost"];
- } else if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
- widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
- }
-
const subscriptionsEndPoint =
widget.fields.includes("activeSubscriptions") || widget.fields.includes("nextRenewingSubscription")
? "get_subscriptions"
diff --git a/src/widgets/wallos/component.test.jsx b/src/widgets/wallos/component.test.jsx
index 7006ed3f2..6d4dc823d 100644
--- a/src/widgets/wallos/component.test.jsx
+++ b/src/widgets/wallos/component.test.jsx
@@ -22,12 +22,7 @@ describe("widgets/wallos/component", () => {
const service = { widget: { type: "wallos" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual([
- "activeSubscriptions",
- "nextRenewingSubscription",
- "thisMonthlyCost",
- "nextMonthlyCost",
- ]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("wallos.activeSubscriptions")).toBeInTheDocument();
expect(screen.getByText("wallos.nextRenewingSubscription")).toBeInTheDocument();
diff --git a/src/widgets/watchtower/component.jsx b/src/widgets/watchtower/component.jsx
index 967ad3e31..5f89ca788 100644
--- a/src/widgets/watchtower/component.jsx
+++ b/src/widgets/watchtower/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/wgeasy/component.jsx b/src/widgets/wgeasy/component.jsx
index 338d94cc1..705f4e9b5 100644
--- a/src/widgets/wgeasy/component.jsx
+++ b/src/widgets/wgeasy/component.jsx
@@ -1,19 +1,18 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
-export default function Component({ service }) {
+const DEFAULT_FIELDS = ["connected", "enabled", "total"];
+
+export default function Component({ service: configuredService }) {
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
const endpoint = widget.version === 2 ? "clientv2" : "client";
const { data: infoData, error: infoError } = useWidgetAPI(widget, endpoint);
- if (!widget.fields) {
- widget.fields = ["connected", "enabled", "total"];
- }
-
if (infoError || infoData?.statusCode > 400) {
return ;
}
diff --git a/src/widgets/wgeasy/component.test.jsx b/src/widgets/wgeasy/component.test.jsx
index eebc83915..8269a0906 100644
--- a/src/widgets/wgeasy/component.test.jsx
+++ b/src/widgets/wgeasy/component.test.jsx
@@ -28,7 +28,7 @@ describe("widgets/wgeasy/component", () => {
const service = { widget: { type: "wgeasy" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["connected", "enabled", "total"]);
+ expect(service.widget.fields).toBeUndefined();
// Container filters by widget.fields; "disabled" is not included by default.
expect(container.querySelectorAll(".service-block")).toHaveLength(3);
expect(screen.getByText("wgeasy.connected")).toBeInTheDocument();
diff --git a/src/widgets/whatsupdocker/component.jsx b/src/widgets/whatsupdocker/component.jsx
index cc3b51740..1b8b3eda5 100644
--- a/src/widgets/whatsupdocker/component.jsx
+++ b/src/widgets/whatsupdocker/component.jsx
@@ -1,6 +1,5 @@
import Block from "components/services/widget/block";
import Container from "components/services/widget/container";
-
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/xteve/component.jsx b/src/widgets/xteve/component.jsx
index 5e2b1c1d5..ca5cf19f1 100644
--- a/src/widgets/xteve/component.jsx
+++ b/src/widgets/xteve/component.jsx
@@ -1,7 +1,7 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
export default function Component({ service }) {
diff --git a/src/widgets/yourspotify/component.jsx b/src/widgets/yourspotify/component.jsx
index b028a9186..4554ea2ff 100644
--- a/src/widgets/yourspotify/component.jsx
+++ b/src/widgets/yourspotify/component.jsx
@@ -1,8 +1,8 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
import { useMemo } from "react";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
function getStartDate(interval) {
diff --git a/src/widgets/zabbix/component.jsx b/src/widgets/zabbix/component.jsx
index 397de7f18..a8788fa00 100644
--- a/src/widgets/zabbix/component.jsx
+++ b/src/widgets/zabbix/component.jsx
@@ -1,8 +1,9 @@
-import Block from "components/services/widget/block";
-import Container from "components/services/widget/container";
import { useTranslation } from "next-i18next/pages";
+import Block from "components/services/widget/block";
+import Container from "components/services/widget/container";
import useWidgetAPI from "utils/proxy/use-widget-api";
+import withWidgetFields from "utils/widget-fields";
const PriorityUnclassified = "0";
const PriorityInformation = "1";
@@ -10,9 +11,11 @@ const PriorityWarning = "2";
const PriorityAverage = "3";
const PriorityHigh = "4";
const PriorityDisaster = "5";
+const DEFAULT_FIELDS = ["warning", "average", "high", "disaster"];
-export default function Component({ service }) {
+export default function Component({ service: configuredService }) {
const { t } = useTranslation();
+ const service = withWidgetFields(configuredService, DEFAULT_FIELDS);
const { widget } = service;
const { data: zabbixData, error: zabbixError } = useWidgetAPI(widget, "trigger");
@@ -21,12 +24,6 @@ export default function Component({ service }) {
return ;
}
- if (!widget.fields) {
- widget.fields = ["warning", "average", "high", "disaster"];
- } else if (widget.fields?.length > 4) {
- widget.fields = widget.fields.slice(0, 4);
- }
-
if (!zabbixData) {
return (
diff --git a/src/widgets/zabbix/component.test.jsx b/src/widgets/zabbix/component.test.jsx
index 25047022a..95e019f04 100644
--- a/src/widgets/zabbix/component.test.jsx
+++ b/src/widgets/zabbix/component.test.jsx
@@ -22,7 +22,7 @@ describe("widgets/zabbix/component", () => {
const service = { widget: { type: "zabbix" } };
const { container } = renderWithProviders(, { settings: { hideErrors: false } });
- expect(service.widget.fields).toEqual(["warning", "average", "high", "disaster"]);
+ expect(service.widget.fields).toBeUndefined();
expect(container.querySelectorAll(".service-block")).toHaveLength(4);
expect(screen.getByText("zabbix.warning")).toBeInTheDocument();
expect(screen.getByText("zabbix.average")).toBeInTheDocument();