diff --git a/.codecov.yml b/.codecov.yml index d9b99dd7a..ce563cbd8 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -9,11 +9,11 @@ coverage: project: default: target: 100% - threshold: 25% + threshold: 15% patch: default: target: 100% - threshold: 25% + threshold: 10% comment: layout: "reach,diff,flags,files" diff --git a/.github/DISCUSSION_TEMPLATE/feature-requests.yml b/.github/DISCUSSION_TEMPLATE/feature-requests.yml index 96e5ef2a5..d12589249 100644 --- a/.github/DISCUSSION_TEMPLATE/feature-requests.yml +++ b/.github/DISCUSSION_TEMPLATE/feature-requests.yml @@ -1,6 +1,10 @@ title: "[Feature Request] " labels: ["enhancement"] body: + - type: markdown + attributes: + value: | + #### ⚠️ Don't forget to search [existing issues](https://github.com/gethomepage/homepage/search?q=&type=issues) and [discussions](https://github.com/gethomepage/homepage/search?q=&type=discussions) (including closed ones!). - type: textarea id: description attributes: diff --git a/.github/DISCUSSION_TEMPLATE/support.yml b/.github/DISCUSSION_TEMPLATE/support.yml index 9edfe3d2f..37617ce36 100644 --- a/.github/DISCUSSION_TEMPLATE/support.yml +++ b/.github/DISCUSSION_TEMPLATE/support.yml @@ -51,7 +51,7 @@ body: id: troubleshooting attributes: label: Troubleshooting - description: Please include output from your [troubleshooting steps](https://gethomepage.dev/more/troubleshooting/#service-widget-errors), if relevant. + description: Please include output from your [troubleshooting steps](https://gethomepage.dev/troubleshooting/#service-widget-errors), if relevant. validations: required: true - type: markdown diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a9d002c37..a3674817c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -35,8 +35,8 @@ What type of change does your PR introduce to Homepage? ## Checklist: - [ ] If applicable, I have added corresponding documentation changes. -- [ ] If applicable, I have added or updated tests for new features and bug fixes. -- [ ] If applicable, I have reviewed the [feature / enhancement](https://gethomepage.dev/more/development/#new-feature-guidelines) and / or [service widget guidelines](https://gethomepage.dev/more/development/#service-widget-guidelines). -- [ ] I have checked that all code style checks pass using [pre-commit hooks](https://gethomepage.dev/more/development/#code-formatting-with-pre-commit-hooks) and [linting checks](https://gethomepage.dev/more/development/#code-linting). +- [ ] If applicable, I have added or updated tests for new features and bug fixes (see [testing](https://gethomepage.dev/widgets/authoring/getting-started/#testing)). +- [ ] If applicable, I have reviewed the [feature / enhancement](https://gethomepage.dev/widgets/authoring/getting-started/#new-feature-guidelines) and / or [service widget guidelines](https://gethomepage.dev/widgets/authoring/getting-started/#service-widget-guidelines). +- [ ] I have checked that all code style checks pass using [pre-commit hooks](https://gethomepage.dev/widgets/authoring/getting-started/#code-formatting-with-pre-commit-hooks) and [linting checks](https://gethomepage.dev/widgets/authoring/getting-started/#code-linting). - [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers. - [ ] In the description above I have disclosed the use of AI tools in the coding of this PR. diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index aba284501..69e2b40ab 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -66,7 +66,7 @@ jobs: - name: Extract Docker metadata id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: | ${{ env.IMAGE_NAME }} @@ -115,7 +115,7 @@ jobs: - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -123,20 +123,20 @@ jobs: - name: Login to Docker Hub if: github.event_name != 'pull_request' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Setup QEMU - uses: docker/setup-qemu-action@v3.7.0 + uses: docker/setup-qemu-action@v4.0.0 - name: Setup Docker buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and push Docker image id: build-and-push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . push: ${{ github.event_name != 'pull_request' }} diff --git a/.github/workflows/pr-quality.yml b/.github/workflows/pr-quality.yml new file mode 100644 index 000000000..a0cc6e497 --- /dev/null +++ b/.github/workflows/pr-quality.yml @@ -0,0 +1,18 @@ +name: PR Quality + +permissions: + contents: read + issues: read + pull-requests: write + +on: + pull_request_target: + types: [opened, reopened] + +jobs: + anti-slop: + runs-on: ubuntu-latest + steps: + - uses: peakoss/anti-slop@v0 + with: + max-failures: 4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b716aab83..5d5234736 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,13 +13,13 @@ jobs: matrix: shard: [1, 2, 3, 4] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4 with: version: 9 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 20 cache: pnpm diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e28744ab8..4029c6b49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,11 +38,11 @@ People _love_ thorough bug reports. I'm not even kidding. ## Development Guidelines -Please see the [documentation regarding development](https://gethomepage.dev/more/development/) and specifically the [guidelines for new service widgets](https://gethomepage.dev/more/development/#service-widget-guidelines) if you are considering making one. +Please see the [documentation regarding development](https://gethomepage.dev/widgets/authoring/getting-started/#development) and specifically the [guidelines for new service widgets](https://gethomepage.dev/widgets/authoring/getting-started/#service-widget-guidelines) if you are considering making one. ## Use a Consistent Coding Style -Please see information in the docs regarding [code formatting with pre-commit hooks](https://gethomepage.dev/more/development/#code-formatting-with-pre-commit-hooks). +Please see information in the docs regarding [code formatting with pre-commit hooks](https://gethomepage.dev/widgets/authoring/getting-started/#code-formatting-with-pre-commit-hooks). ## License diff --git a/docs/configs/docker.md b/docs/configs/docker.md index 0fffc9626..32d01c0f6 100644 --- a/docs/configs/docker.md +++ b/docs/configs/docker.md @@ -177,6 +177,16 @@ labels: - homepage.widget.fields=["field1","field2"] # optional ``` +!!! note + + If you use mapping syntax (`:`) for labels instead of list syntax (`-`), array values like `fields` must be wrapped in single quotes so they are passed as a string: + + ```yaml + labels: + ... + homepage.widget.fields: '["field1","field2"]' + ``` + Multiple widgets can be specified by incrementing the index, e.g. ```yaml diff --git a/docs/installation/k8s.md b/docs/installation/k8s.md index 172b9b295..010a8749e 100644 --- a/docs/installation/k8s.md +++ b/docs/installation/k8s.md @@ -223,13 +223,33 @@ spec: - name: homepage image: "ghcr.io/gethomepage/homepage:latest" imagePullPolicy: Always + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault env: + - name: MY_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP - name: HOMEPAGE_ALLOWED_HOSTS - value: gethomepage.dev # required, may need port. See gethomepage.dev/installation/#homepage_allowed_hosts + value: "$(MY_POD_IP):3000,gethomepage.dev" # See gethomepage.dev/installation/#homepage_allowed_hosts . Value before the comma is required for the k8s probe ports: - name: http containerPort: 3000 protocol: TCP + livenessProbe: + httpGet: + path: /api/healthcheck + port: http + initialDelaySeconds: 5 + periodSeconds: 15 volumeMounts: - mountPath: /app/config/custom.js name: homepage-config diff --git a/docs/widgets/info/resources.md b/docs/widgets/info/resources.md index 7fcf9c5cd..08ab95abf 100644 --- a/docs/widgets/info/resources.md +++ b/docs/widgets/info/resources.md @@ -7,13 +7,17 @@ You can include all or some of the available resources. If you do not want to se The disk path is the path reported by `df` (Mounted On), or the mount point of the disk. +!!! note + + Any disk you wish to access must be mounted to your container as a volume. + The cpu and memory resource information are the container's usage while [glances](glances.md) displays statistics for the host machine on which it is installed. The resources widget primarily relies on a popular tool called [systeminformation](https://systeminformation.io). Thus, any limitiations of that software apply, for example, BRTFS RAID is not supported for the disk usage. In this case users may want to use the [glances widget](glances.md) instead. -_Note: unfortunately, the package used for getting CPU temp ([systeminformation](https://systeminformation.io)) is not compatible with some setups and will not report any value(s) for CPU temp._ +!!! warning -**Any disk you wish to access must be mounted to your container as a volume.** + The package used for getting CPU temp ([systeminformation](https://systeminformation.io)) is not compatible with some setups and will not report any value(s) for CPU temp. ```yaml - resources: @@ -75,3 +79,10 @@ You can additionally supply an optional `expanded` property set to true in order ``` ![194136533-c4238c82-4d67-41a4-b3c8-18bf26d33ac2](https://user-images.githubusercontent.com/3441425/194728642-a9885274-922b-4027-acf5-a746f58fdfce.png) + +To monitor a named host network interface in Docker (for example `network: eno1`), mount host `/sys` (read-only): + +```yaml +volumes: + - /sys:/sys:ro +``` diff --git a/docs/widgets/services/index.md b/docs/widgets/services/index.md index 4aa67bdd6..e982fb9b9 100644 --- a/docs/widgets/services/index.md +++ b/docs/widgets/services/index.md @@ -67,7 +67,7 @@ You can also find a list of all available service widgets in the sidebar navigat - [Jackett](jackett.md) - [JDownloader](jdownloader.md) - [Jellyfin](jellyfin.md) -- [Jellyseerr](jellyseerr.md) +- [Seerr](seerr.md) - [Jellystat](jellystat.md) - [Kavita](kavita.md) - [Komga](komga.md) @@ -101,7 +101,6 @@ You can also find a list of all available service widgets in the sidebar navigat - [OpenMediaVault](openmediavault.md) - [OpenWRT](openwrt.md) - [OPNsense](opnsense.md) -- [Overseerr](overseerr.md) - [PaperlessNGX](paperlessngx.md) - [Peanut](peanut.md) - [pfSense](pfsense.md) diff --git a/docs/widgets/services/jellyfin.md b/docs/widgets/services/jellyfin.md index 8849e0645..ddf79a6c5 100644 --- a/docs/widgets/services/jellyfin.md +++ b/docs/widgets/services/jellyfin.md @@ -5,7 +5,7 @@ description: Jellyfin Widget Configuration Learn more about [Jellyfin](https://github.com/jellyfin/jellyfin). -You can create an API key from inside Jellyfin at `Settings > Advanced > Api Keys`. +You can create an API key from inside the Jellyfin Administration Dashboard under `Advanced > API Keys`. As of v0.6.11 the widget supports fields `["movies", "series", "episodes", "songs"]`. These blocks are disabled by default but can be enabled with the `enableBlocks` option, and the "Now Playing" feature (enabled by default) can be disabled with the `enableNowPlaying` option. @@ -17,7 +17,7 @@ As of v0.6.11 the widget supports fields `["movies", "series", "episodes", "song ```yaml widget: type: jellyfin - url: http://jellyfin.host.or.ip + url: http://jellyfin.host.or.ip:port key: apikeyapikeyapikeyapikeyapikey version: 2 # optional, default is 1 enableBlocks: true # optional, defaults to false diff --git a/docs/widgets/services/jellyseerr.md b/docs/widgets/services/jellyseerr.md deleted file mode 100644 index 8ffe5fbdf..000000000 --- a/docs/widgets/services/jellyseerr.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Jellyseerr -description: Jellyseerr Widget Configuration ---- - -Learn more about [Jellyseerr](https://github.com/Fallenbagel/jellyseerr). - -Find your API key under `Settings > General > API Key`. - -Allowed fields: `["pending", "approved", "available", "issues"]`. -Default fields: `["pending", "approved", "available"]`. - -```yaml -widget: - type: jellyseerr - url: http://jellyseerr.host.or.ip - key: apikeyapikeyapikeyapikeyapikey -``` diff --git a/docs/widgets/services/overseerr.md b/docs/widgets/services/overseerr.md deleted file mode 100644 index 4d3d6bb1d..000000000 --- a/docs/widgets/services/overseerr.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Overseerr -description: Overseerr Widget Configuration ---- - -Learn more about [Overseerr](https://github.com/sct/overseerr). - -Find your API key under `Settings > General`. - -Allowed fields: `["pending", "approved", "available", "processing"]`. - -```yaml -widget: - type: overseerr - url: http://overseerr.host.or.ip - key: apikeyapikeyapikeyapikeyapikey -``` diff --git a/docs/widgets/services/plex-tautulli.md b/docs/widgets/services/plex-tautulli.md index 9cacdf057..c1f164eb2 100644 --- a/docs/widgets/services/plex-tautulli.md +++ b/docs/widgets/services/plex-tautulli.md @@ -12,7 +12,7 @@ Allowed fields: no configurable fields for this widget. ```yaml widget: type: tautulli - url: http://tautulli.host.or.ip + url: http://tautulli.host.or.ip:port key: apikeyapikeyapikeyapikeyapikey enableUser: true # optional, defaults to false showEpisodeNumber: true # optional, defaults to false diff --git a/docs/widgets/services/seerr.md b/docs/widgets/services/seerr.md new file mode 100644 index 000000000..1067b3017 --- /dev/null +++ b/docs/widgets/services/seerr.md @@ -0,0 +1,20 @@ +--- +title: Seerr Widget +description: Seerr Widget Configuration +--- + +Learn more about [Seerr](https://github.com/seerr-team/seerr). + +Find your API key under `Settings > General > API Key`. + +_Jellyseerr and Overseerr merged into Seerr. Use `type: seerr` (legacy `type: jellyseerr` and `type: overseerr` are aliased)._ + +Allowed fields: `["pending", "approved", "available", "completed", "processing", "issues"]`. +Default fields: `["pending", "approved", "completed"]`. + +```yaml +widget: + type: seerr + url: http://seerr.host.or.ip + key: apikeyapikeyapikeyapikeyapikey +``` diff --git a/docs/widgets/services/sparkyfitness.md b/docs/widgets/services/sparkyfitness.md new file mode 100644 index 000000000..879be8870 --- /dev/null +++ b/docs/widgets/services/sparkyfitness.md @@ -0,0 +1,15 @@ +--- +title: SparkyFitness +description: SparkyFitness Widget Configuration +--- + +Learn more about [SparkyFitness](https://github.com/CodeWithCJ/SparkyFitness). + +Allowed fields: `["eaten", "burned", "remaining", "steps"]`. + +```yaml +widget: + type: sparkyfitness + url: http://sparkyfitness.host.or.ip + key: apikeyapikeyapikeyapikeyapikey +``` diff --git a/docs/widgets/services/tracearr.md b/docs/widgets/services/tracearr.md new file mode 100644 index 000000000..c53f8aca1 --- /dev/null +++ b/docs/widgets/services/tracearr.md @@ -0,0 +1,21 @@ +--- +title: Tracearr +description: Tracearr Widget Configuration +--- + +Learn more about [Tracearr](https://www.tracearr.com/). + +Provides detailed information about currently active streams across multiple servers. + +Allowed fields (for summary view): `["streams", "transcodes", "directplay", "bitrate"]`. + +```yaml +widget: + type: tracearr + url: http://tracearr.host.or.ip:3000 + key: apikeyapikeyapikeyapikeyapikey + view: both # optional, "summary", "details", or "both", defaults to "details" + enableUser: true # optional, defaults to false + showEpisodeNumber: true # optional, defaults to false + expandOneStreamToTwoRows: false # optional, defaults to true +``` diff --git a/mkdocs.yml b/mkdocs.yml index 6b240aee4..b08277808 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -91,7 +91,6 @@ nav: - widgets/services/jackett.md - widgets/services/jdownloader.md - widgets/services/jellyfin.md - - widgets/services/jellyseerr.md - widgets/services/jellystat.md - widgets/services/kavita.md - widgets/services/komga.md @@ -125,7 +124,6 @@ nav: - widgets/services/openmediavault.md - widgets/services/opnsense.md - widgets/services/openwrt.md - - widgets/services/overseerr.md - widgets/services/pangolin.md - widgets/services/paperlessngx.md - widgets/services/peanut.md @@ -151,8 +149,10 @@ nav: - widgets/services/rutorrent.md - widgets/services/sabnzbd.md - widgets/services/scrutiny.md + - widgets/services/seerr.md - widgets/services/slskd.md - widgets/services/sonarr.md + - widgets/services/sparkyfitness.md - widgets/services/speedtest-tracker.md - widgets/services/spoolman.md - widgets/services/stash.md @@ -165,6 +165,7 @@ nav: - widgets/services/technitium.md - widgets/services/tdarr.md - widgets/services/traefik.md + - widgets/services/tracearr.md - widgets/services/transmission.md - widgets/services/trilium.md - widgets/services/truenas.md diff --git a/package.json b/package.json index f502fc7d6..6b8ff4c9c 100644 --- a/package.json +++ b/package.json @@ -22,19 +22,19 @@ "follow-redirects": "^1.15.11", "gamedig": "^5.3.2", "i18next": "^25.8.0", - "ical.js": "^2.1.0", + "ical.js": "^2.2.1", "js-yaml": "^4.1.1", "json-rpc-2.0": "^1.7.0", "luxon": "^3.6.1", "memory-cache": "^0.2.0", "minecraftstatuspinger": "^1.2.2", "next": "^15.5.11", - "next-i18next": "^12.1.0", + "next-i18next": "^15.4.3", "ping": "^0.4.4", "pretty-bytes": "^7.1.0", "raw-body": "^3.0.2", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", "react-i18next": "^15.5.3", "react-icons": "^5.5.0", "recharts": "^3.1.2", @@ -63,9 +63,9 @@ "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-react": "^7.37.4", "eslint-plugin-react-hooks": "^5.2.0", - "jsdom": "^26.1.0", + "jsdom": "^28.1.0", "postcss": "^8.5.6", - "prettier": "^3.7.3", + "prettier": "^3.8.1", "prettier-plugin-organize-imports": "^4.3.0", "tailwind-scrollbar": "^4.0.2", "tailwindcss": "^4.1.18", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84e4dabaf..a2677f56f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ importers: dependencies: '@headlessui/react': specifier: ^2.2.9 - version: 2.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@kubernetes/client-node': specifier: ^1.0.0 version: 1.0.0 @@ -33,8 +33,8 @@ importers: specifier: ^25.8.0 version: 25.8.0(typescript@5.7.3) ical.js: - specifier: ^2.1.0 - version: 2.1.0 + specifier: ^2.2.1 + version: 2.2.1 js-yaml: specifier: ^4.1.1 version: 4.1.1 @@ -52,10 +52,10 @@ importers: version: 1.2.2 next: specifier: ^15.5.11 - version: 15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.5.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4) next-i18next: - specifier: ^12.1.0 - version: 12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + specifier: ^15.4.3 + version: 15.4.3(@types/react@19.0.10)(i18next@25.8.0(typescript@5.7.3))(next@15.5.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-i18next@15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3))(react@19.2.4) ping: specifier: ^0.4.4 version: 0.4.4 @@ -66,23 +66,23 @@ importers: specifier: ^3.0.2 version: 3.0.2 react: - specifier: ^18.3.1 - version: 18.3.1 + specifier: ^19.2.4 + version: 19.2.4 react-dom: - specifier: ^18.3.1 - version: 18.3.1(react@18.3.1) + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) react-i18next: specifier: ^15.5.3 - version: 15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3) + version: 15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3) react-icons: specifier: ^5.5.0 - version: 5.5.0(react@18.3.1) + version: 5.5.0(react@19.2.4) recharts: specifier: ^3.1.2 - version: 3.1.2(@types/react@19.0.10)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(redux@5.0.1) + version: 3.1.2(@types/react@19.0.10)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1) swr: specifier: ^2.4.0 - version: 2.4.0(react@18.3.1) + version: 2.4.0(react@19.2.4) systeminformation: specifier: ^5.27.11 version: 5.27.11 @@ -122,10 +122,10 @@ importers: version: 6.9.1 '@testing-library/react': specifier: ^16.3.0 - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react@19.0.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react@19.0.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)) + version: 3.2.4(vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2)) eslint: specifier: ^9.25.1 version: 9.25.1(jiti@2.6.1) @@ -143,7 +143,7 @@ importers: version: 6.10.2(eslint@9.25.1(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.5.4 - version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.25.1(jiti@2.6.1)))(eslint@9.25.1(jiti@2.6.1))(prettier@3.7.3) + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.25.1(jiti@2.6.1)))(eslint@9.25.1(jiti@2.6.1))(prettier@3.8.1) eslint-plugin-react: specifier: ^7.37.4 version: 7.37.4(eslint@9.25.1(jiti@2.6.1)) @@ -151,20 +151,20 @@ importers: specifier: ^5.2.0 version: 5.2.0(eslint@9.25.1(jiti@2.6.1)) jsdom: - specifier: ^26.1.0 - version: 26.1.0 + specifier: ^28.1.0 + version: 28.1.0 postcss: specifier: ^8.5.6 version: 8.5.6 prettier: - specifier: ^3.7.3 - version: 3.7.3 + specifier: ^3.8.1 + version: 3.8.1 prettier-plugin-organize-imports: specifier: ^4.3.0 - version: 4.3.0(prettier@3.7.3)(typescript@5.7.3) + version: 4.3.0(prettier@3.8.1)(typescript@5.7.3) tailwind-scrollbar: specifier: ^4.0.2 - version: 4.0.2(react@18.3.1)(tailwindcss@4.1.18) + version: 4.0.2(react@19.2.4)(tailwindcss@4.1.18) tailwindcss: specifier: ^4.1.18 version: 4.1.18 @@ -173,7 +173,7 @@ importers: version: 5.7.3 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2) + version: 3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2) optionalDependencies: osx-temperature-sensor: specifier: ^1.0.8 @@ -181,6 +181,9 @@ importers: packages: + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + '@adobe/css-tools@4.4.4': resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} @@ -192,8 +195,15 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@asamuzakjp/css-color@3.2.0': - resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@asamuzakjp/css-color@5.0.1': + resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} @@ -212,10 +222,6 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - '@babel/runtime@7.26.9': - resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.27.6': resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} @@ -235,37 +241,44 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} - '@csstools/color-helpers@5.1.0': - resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} - engines: {node: '>=18'} + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} - '@csstools/css-calc@2.1.4': - resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} - engines: {node: '>=18'} + '@csstools/css-calc@3.1.1': + resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@3.1.0': - resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} - engines: {node: '>=18'} + '@csstools/css-color-parser@4.0.2': + resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-parser-algorithms@3.0.5': - resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} - engines: {node: '>=18'} + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-tokenizer@3.0.4': - resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} - engines: {node: '>=18'} + '@csstools/css-syntax-patches-for-csstree@1.0.29': + resolution: {integrity: sha512-jx9GjkkP5YHuTmko2eWAvpPnb0mB4mGRr2U7XwVNwevm8nlpobZEVk+GNmiYMk2VuA75v+plfXWyroWKmICZXg==} + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} '@dabh/diagnostics@2.0.8': resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} @@ -490,6 +503,15 @@ packages: resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -1213,8 +1235,10 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/hoist-non-react-statics@3.3.6': - resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} + '@types/hoist-non-react-statics@3.3.7': + resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} + peerDependencies: + '@types/react': '*' '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -1554,6 +1578,9 @@ packages: bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} @@ -1683,8 +1710,8 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - core-js@3.40.0: - resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==} + core-js@3.48.0: + resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -1697,15 +1724,19 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} - cssstyle@4.6.0: - resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} - engines: {node: '>=18'} + cssstyle@6.2.0: + resolution: {integrity: sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==} + engines: {node: '>=20'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} @@ -1754,9 +1785,9 @@ packages: damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - data-urls@5.0.0: - resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} - engines: {node: '>=18'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} @@ -1796,6 +1827,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decimal.js-light@2.5.1: resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} @@ -2252,6 +2292,7 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@14.0.0: @@ -2306,9 +2347,9 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - html-encoding-sniffer@4.0.0: - resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} - engines: {node: '>=18'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -2335,11 +2376,8 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - i18next-fs-backend@1.2.0: - resolution: {integrity: sha512-pUx3AcgXCbur0jpFA7U67Z2RJflAcIi698Y8VL+phdOqUchahxriV3Cs+M6UkPNQSS/zPEzWLfdJ8EgjB7HVxg==} - - i18next@21.10.0: - resolution: {integrity: sha512-YeuIBmFsGjUfO3qBmMOc0rQaun4mIpGKET5WDwvu8lU7gvwpcariZLNtL0Fzj+zazcHUrlXHiptcFhBMFaxzfg==} + i18next-fs-backend@2.6.1: + resolution: {integrity: sha512-eYWTX7QT7kJ0sZyCPK6x1q+R63zvNKv2D6UdbMf15A8vNb2ZLyw4NNNZxPFwXlIv/U+oUtg8SakW6ZgJZcoqHQ==} i18next@25.8.0: resolution: {integrity: sha512-urrg4HMFFMQZ2bbKRK7IZ8/CTE7D8H4JRlAwqA2ZwDRFfdd0K/4cdbNNLgfn9mo+I/h9wJu61qJzH7jCFAhUZQ==} @@ -2349,12 +2387,8 @@ packages: typescript: optional: true - ical.js@2.1.0: - resolution: {integrity: sha512-BOVfrH55xQ6kpS3muGvIXIg2l7p+eoe12/oS7R5yrO3TL/j/bLsR0PR+tYQESFbyTbvGgPHn9zQ6tI4FWyuSaQ==} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} + ical.js@2.2.1: + resolution: {integrity: sha512-yK/UlPbEs316igb/tjRgbFA8ZV75rCsBJp/hWOatpyaPNlgw0dGDmU+FoicOcwX4xXkeXOkYiOmCqNPFpNPkQg==} iconv-lite@0.7.0: resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} @@ -2567,9 +2601,9 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - jsdom@26.1.0: - resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} - engines: {node: '>=18'} + jsdom@28.1.0: + resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: @@ -2723,6 +2757,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + luxon@3.6.1: resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} engines: {node: '>=12'} @@ -2745,6 +2783,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + memory-cache@0.2.0: resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==} @@ -2831,12 +2872,14 @@ packages: net@1.0.2: resolution: {integrity: sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==} - next-i18next@12.1.0: - resolution: {integrity: sha512-rhos/PVULmZPdC0jpec2MDBQMXdGZ3+Mbh/tZfrDtjgnVN3ucdq7k8BlwsJNww6FnqC8AC31n6dSYuqVzYsGsw==} - engines: {node: '>=12'} + next-i18next@15.4.3: + resolution: {integrity: sha512-ZRmiz72o1Jvh2ZghCUQX1Ua5F/f2W1/Ila/L1ZeKVuSWiH7J4zfUedfDxNBEhj9lajREC7aoJuPXMFtKi2bdIg==} + engines: {node: '>=14'} peerDependencies: - next: '>= 10.0.0' - react: '>= 16.8.0' + i18next: '>= 23.7.13' + next: '>= 12.0.0' + react: '>= 17.0.2' + react-i18next: '>= 13.5.0' next@15.5.11: resolution: {integrity: sha512-L2KPiKmqTDpRdeVDdPjhf43g2/VPe0NCNndq7OKDCgOLWtxe1kbr/zXGIZtYY7kZEAjRf7Bj/mwUFSr+tYC2Yg==} @@ -2872,9 +2915,6 @@ packages: resolution: {integrity: sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==} engines: {node: '>=14.16'} - nwsapi@2.2.23: - resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} - oauth4webapi@3.3.0: resolution: {integrity: sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==} @@ -2951,8 +2991,8 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} @@ -3025,8 +3065,8 @@ packages: vue-tsc: optional: true - prettier@3.7.3: - resolution: {integrity: sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg==} + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} engines: {node: '>=14'} hasBin: true @@ -3071,23 +3111,10 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} - react-dom@18.3.1: - resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: - react: ^18.3.1 - - react-i18next@11.18.6: - resolution: {integrity: sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==} - peerDependencies: - i18next: '>= 19.0.0' - react: '>= 16.8.0' - react-dom: '*' - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true + react: ^19.2.4 react-i18next@15.5.3: resolution: {integrity: sha512-ypYmOKOnjqPEJZO4m1BI0kS8kWqkBNsKYyhVUfij0gvjy9xJNoG/VcGkxq5dRlVwzmrmY1BQMAmpbbUBLwC4Kw==} @@ -3131,8 +3158,8 @@ packages: redux: optional: true - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} readable-stream@1.0.34: @@ -3169,9 +3196,6 @@ packages: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} @@ -3180,6 +3204,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -3222,9 +3250,6 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rrweb-cssom@0.8.0: - resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} - run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3263,8 +3288,8 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} seek-bzip@2.0.0: resolution: {integrity: sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==} @@ -3501,7 +3526,7 @@ packages: tar@7.4.3: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me telnet-client@2.2.6: resolution: {integrity: sha512-ZUYrLsPtQupQww3eSEORDVOb6ztdtKEghya6TVXPo2tg/UQq2pn5rHhvwuUvyYpbnsoqdNY1fyD1GNkXHR8dYA==} @@ -3542,16 +3567,9 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} - tldts-core@6.1.86: - resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} - tldts-core@7.0.12: resolution: {integrity: sha512-3K76aXywJFduGRsOYoY5JzINLs/WMlOkeDwPL+8OCPq2Rh39gkSDtWAxdJQlWjpun/xF/LHf29yqCi6VC/rHDA==} - tldts@6.1.86: - resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} - hasBin: true - tldts@7.0.12: resolution: {integrity: sha512-M9ZQBPp6FyqhMcl233vHYyYRkxXOA1SKGlnq13S0mJdUhRSwr2w6I8rlchPL73wBwRlyIZpFvpu2VcdSMWLYXw==} hasBin: true @@ -3571,10 +3589,6 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - tough-cookie@5.1.2: - resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} - engines: {node: '>=16'} - tough-cookie@6.0.0: resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} engines: {node: '>=16'} @@ -3582,9 +3596,9 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} triple-beam@1.4.1: resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} @@ -3640,6 +3654,10 @@ packages: undici-types@7.8.0: resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} + engines: {node: '>=20.18.1'} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -3760,22 +3778,17 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} - whatwg-encoding@3.1.1: - resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} - engines: {node: '>=18'} - deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - - whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -3886,6 +3899,8 @@ packages: snapshots: + '@acemir/cssom@0.9.31': {} + '@adobe/css-tools@4.4.4': {} '@alloc/quick-lru@5.2.0': {} @@ -3895,13 +3910,23 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@asamuzakjp/css-color@3.2.0': + '@asamuzakjp/css-color@5.0.1': dependencies: - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - lru-cache: 10.4.3 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.2.6 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.6 + + '@asamuzakjp/nwsapi@2.3.9': {} '@babel/code-frame@7.29.0': dependencies: @@ -3917,10 +3942,6 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/runtime@7.26.9': - dependencies: - regenerator-runtime: 0.14.1 - '@babel/runtime@7.27.6': {} '@babel/runtime@7.28.6': {} @@ -3934,27 +3955,33 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.1.0 + '@colors/colors@1.6.0': {} - '@csstools/color-helpers@5.1.0': {} + '@csstools/color-helpers@6.0.2': {} - '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/color-helpers': 5.1.0 - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-syntax-patches-for-csstree@1.0.29': {} + + '@csstools/css-tokenizer@4.0.0': {} '@dabh/diagnostics@2.0.8': dependencies: @@ -4112,6 +4139,8 @@ snapshots: '@eslint/core': 0.13.0 levn: 0.4.1 + '@exodus/bytes@1.15.0': {} + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -4121,18 +4150,18 @@ snapshots: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@floating-ui/react-dom@2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/dom': 1.7.4 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@floating-ui/react@0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@floating-ui/react@0.26.28(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@floating-ui/utils': 0.2.10 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) tabbable: 6.3.0 '@floating-ui/utils@0.2.10': {} @@ -4149,15 +4178,15 @@ snapshots: protobufjs: 7.5.3 yargs: 17.7.2 - '@headlessui/react@2.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@headlessui/react@2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@floating-ui/react': 0.26.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/focus': 3.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/interactions': 3.25.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@tanstack/react-virtual': 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - use-sync-external-store: 1.6.0(react@18.3.1) + '@floating-ui/react': 0.26.28(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-aria/focus': 3.21.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-aria/interactions': 3.25.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/react-virtual': 3.13.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) '@humanfs/core@0.19.1': {} @@ -4417,56 +4446,56 @@ snapshots: '@protobufjs/utf8@1.1.0': {} - '@react-aria/focus@3.21.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/focus@3.21.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@react-aria/interactions': 3.25.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-aria/utils': 3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@react-types/shared': 3.32.1(react@18.3.1) + '@react-aria/interactions': 3.25.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-aria/utils': 3.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-types/shared': 3.32.1(react@19.2.4) '@swc/helpers': 0.5.17 clsx: 2.1.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@react-aria/interactions@3.25.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/interactions@3.25.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@react-aria/ssr': 3.9.10(react@18.3.1) - '@react-aria/utils': 3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-aria/ssr': 3.9.10(react@19.2.4) + '@react-aria/utils': 3.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-stately/flags': 3.1.2 - '@react-types/shared': 3.32.1(react@18.3.1) + '@react-types/shared': 3.32.1(react@19.2.4) '@swc/helpers': 0.5.17 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@react-aria/ssr@3.9.10(react@18.3.1)': + '@react-aria/ssr@3.9.10(react@19.2.4)': dependencies: '@swc/helpers': 0.5.17 - react: 18.3.1 + react: 19.2.4 - '@react-aria/utils@3.31.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@react-aria/utils@3.31.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@react-aria/ssr': 3.9.10(react@18.3.1) + '@react-aria/ssr': 3.9.10(react@19.2.4) '@react-stately/flags': 3.1.2 - '@react-stately/utils': 3.10.8(react@18.3.1) - '@react-types/shared': 3.32.1(react@18.3.1) + '@react-stately/utils': 3.10.8(react@19.2.4) + '@react-types/shared': 3.32.1(react@19.2.4) '@swc/helpers': 0.5.17 clsx: 2.1.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@react-stately/flags@3.1.2': dependencies: '@swc/helpers': 0.5.17 - '@react-stately/utils@3.10.8(react@18.3.1)': + '@react-stately/utils@3.10.8(react@19.2.4)': dependencies: '@swc/helpers': 0.5.17 - react: 18.3.1 + react: 19.2.4 - '@react-types/shared@3.32.1(react@18.3.1)': + '@react-types/shared@3.32.1(react@19.2.4)': dependencies: - react: 18.3.1 + react: 19.2.4 - '@reduxjs/toolkit@2.9.0(react-redux@9.2.0(@types/react@19.0.10)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + '@reduxjs/toolkit@2.9.0(react-redux@9.2.0(@types/react@19.0.10)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': dependencies: '@standard-schema/spec': 1.0.0 '@standard-schema/utils': 0.3.0 @@ -4475,8 +4504,8 @@ snapshots: redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.1.1 optionalDependencies: - react: 18.3.1 - react-redux: 9.2.0(@types/react@19.0.10)(react@18.3.1)(redux@5.0.1) + react: 19.2.4 + react-redux: 9.2.0(@types/react@19.0.10)(react@19.2.4)(redux@5.0.1) '@rollup/rollup-android-arm-eabi@4.57.1': optional: true @@ -4654,11 +4683,11 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 - '@tanstack/react-virtual@3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@tanstack/react-virtual@3.13.12(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/virtual-core': 3.13.12 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@tanstack/virtual-core@3.13.12': {} @@ -4682,12 +4711,12 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react@19.0.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react@19.0.10)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@babel/runtime': 7.28.6 '@testing-library/dom': 10.4.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) optionalDependencies: '@types/react': 19.0.10 @@ -4733,7 +4762,7 @@ snapshots: '@types/estree@1.0.8': {} - '@types/hoist-non-react-statics@3.3.6': + '@types/hoist-non-react-statics@3.3.7(@types/react@19.0.10)': dependencies: '@types/react': 19.0.10 hoist-non-react-statics: 3.3.2 @@ -4763,7 +4792,7 @@ snapshots: '@types/react@19.0.10': dependencies: - csstype: 3.1.3 + csstype: 3.2.3 '@types/stream-buffers@3.0.7': dependencies: @@ -4820,7 +4849,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.7.3) '@typescript-eslint/utils': 8.29.0(eslint@9.25.1(jiti@2.6.1))(typescript@5.7.3) - debug: 4.4.1 + debug: 4.4.3 eslint: 9.25.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.7.3) typescript: 5.7.3 @@ -4833,7 +4862,7 @@ snapshots: dependencies: '@typescript-eslint/types': 8.29.0 '@typescript-eslint/visitor-keys': 8.29.0 - debug: 4.4.1 + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 @@ -4906,7 +4935,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.3.3': optional: true - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -4921,7 +4950,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2) + vitest: 3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2) transitivePeerDependencies: - supports-color @@ -5124,6 +5153,10 @@ snapshots: dependencies: tweetnacl: 0.14.5 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + bl@4.1.0: dependencies: buffer: 5.7.1 @@ -5255,7 +5288,7 @@ snapshots: concat-map@0.0.1: {} - core-js@3.40.0: {} + core-js@3.48.0: {} core-util-is@1.0.3: {} @@ -5271,14 +5304,21 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + css.escape@1.5.1: {} - cssstyle@4.6.0: + cssstyle@6.2.0: dependencies: - '@asamuzakjp/css-color': 3.2.0 - rrweb-cssom: 0.8.0 + '@asamuzakjp/css-color': 5.0.1 + '@csstools/css-syntax-patches-for-csstree': 1.0.29 + css-tree: 3.1.0 + lru-cache: 11.2.6 - csstype@3.1.3: {} + csstype@3.2.3: {} d3-array@3.2.4: dependencies: @@ -5320,10 +5360,12 @@ snapshots: damerau-levenshtein@1.0.8: {} - data-urls@5.0.0: + data-urls@7.0.0: dependencies: - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' data-view-buffer@1.0.2: dependencies: @@ -5355,6 +5397,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decimal.js-light@2.5.1: {} decimal.js@10.6.0: {} @@ -5743,10 +5789,10 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.25.1(jiti@2.6.1)))(eslint@9.25.1(jiti@2.6.1))(prettier@3.7.3): + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.25.1(jiti@2.6.1)))(eslint@9.25.1(jiti@2.6.1))(prettier@3.8.1): dependencies: eslint: 9.25.1(jiti@2.6.1) - prettier: 3.7.3 + prettier: 3.8.1 prettier-linter-helpers: 1.0.0 synckit: 0.11.11 optionalDependencies: @@ -6094,9 +6140,11 @@ snapshots: dependencies: react-is: 16.13.1 - html-encoding-sniffer@4.0.0: + html-encoding-sniffer@6.0.0: dependencies: - whatwg-encoding: 3.1.1 + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' html-escaper@2.0.2: {} @@ -6117,7 +6165,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -6129,15 +6177,11 @@ snapshots: https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color - i18next-fs-backend@1.2.0: {} - - i18next@21.10.0: - dependencies: - '@babel/runtime': 7.28.6 + i18next-fs-backend@2.6.1: {} i18next@25.8.0(typescript@5.7.3): dependencies: @@ -6145,11 +6189,7 @@ snapshots: optionalDependencies: typescript: 5.7.3 - ical.js@2.1.0: {} - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 + ical.js@2.2.1: {} iconv-lite@0.7.0: dependencies: @@ -6359,32 +6399,32 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@26.1.0: + jsdom@28.1.0: dependencies: - cssstyle: 4.6.0 - data-urls: 5.0.0 + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@bramus/specificity': 2.4.2 + '@exodus/bytes': 1.15.0 + cssstyle: 6.2.0 + data-urls: 7.0.0 decimal.js: 10.6.0 - html-encoding-sniffer: 4.0.0 + html-encoding-sniffer: 6.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.23 - parse5: 7.3.0 - rrweb-cssom: 0.8.0 + parse5: 8.0.0 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 5.1.2 + tough-cookie: 6.0.0 + undici: 7.22.0 w3c-xmlserializer: 5.0.0 - webidl-conversions: 7.0.0 - whatwg-encoding: 3.1.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 14.2.0 - ws: 8.18.3 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - - bufferutil + - '@noble/hashes' - supports-color - - utf-8-validate jsep@1.4.0: {} @@ -6508,6 +6548,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.6: {} + luxon@3.6.1: {} lz-string@1.5.0: {} @@ -6528,6 +6570,8 @@ snapshots: math-intrinsics@1.1.0: {} + mdn-data@2.12.2: {} + memory-cache@0.2.0: {} merge2@1.4.1: {} @@ -6587,30 +6631,29 @@ snapshots: net@1.0.2: {} - next-i18next@12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next-i18next@15.4.3(@types/react@19.0.10)(i18next@25.8.0(typescript@5.7.3))(next@15.5.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-i18next@15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3))(react@19.2.4): dependencies: - '@babel/runtime': 7.26.9 - '@types/hoist-non-react-statics': 3.3.6 - core-js: 3.40.0 + '@babel/runtime': 7.28.6 + '@types/hoist-non-react-statics': 3.3.7(@types/react@19.0.10) + core-js: 3.48.0 hoist-non-react-statics: 3.3.2 - i18next: 21.10.0 - i18next-fs-backend: 1.2.0 - next: 15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - react: 18.3.1 - react-i18next: 11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + i18next: 25.8.0(typescript@5.7.3) + i18next-fs-backend: 2.6.1 + next: 15.5.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-i18next: 15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3) transitivePeerDependencies: - - react-dom - - react-native + - '@types/react' - next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.5.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 15.5.11 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001760 postcss: 8.4.31 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(react@18.3.1) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + styled-jsx: 5.1.6(react@19.2.4) optionalDependencies: '@next/swc-darwin-arm64': 15.5.7 '@next/swc-darwin-x64': 15.5.7 @@ -6631,8 +6674,6 @@ snapshots: normalize-url@8.1.0: {} - nwsapi@2.2.23: {} - oauth4webapi@3.3.0: {} object-assign@4.1.1: {} @@ -6723,7 +6764,7 @@ snapshots: dependencies: callsites: 3.1.0 - parse5@7.3.0: + parse5@8.0.0: dependencies: entities: 6.0.1 @@ -6772,12 +6813,12 @@ snapshots: dependencies: fast-diff: 1.3.0 - prettier-plugin-organize-imports@4.3.0(prettier@3.7.3)(typescript@5.7.3): + prettier-plugin-organize-imports@4.3.0(prettier@3.8.1)(typescript@5.7.3): dependencies: - prettier: 3.7.3 + prettier: 3.8.1 typescript: 5.7.3 - prettier@3.7.3: {} + prettier@3.8.1: {} pretty-bytes@7.1.0: {} @@ -6787,11 +6828,11 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 - prism-react-renderer@2.4.1(react@18.3.1): + prism-react-renderer@2.4.1(react@19.2.4): dependencies: '@types/prismjs': 1.26.5 clsx: 2.1.1 - react: 18.3.1 + react: 19.2.4 process-nextick-args@2.0.1: {} @@ -6834,34 +6875,24 @@ snapshots: iconv-lite: 0.7.0 unpipe: 1.0.0 - react-dom@18.3.1(react@18.3.1): + react-dom@19.2.4(react@19.2.4): dependencies: - loose-envify: 1.4.0 - react: 18.3.1 - scheduler: 0.23.2 + react: 19.2.4 + scheduler: 0.27.0 - react-i18next@11.18.6(i18next@21.10.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@babel/runtime': 7.28.6 - html-parse-stringify: 3.0.1 - i18next: 21.10.0 - react: 18.3.1 - optionalDependencies: - react-dom: 18.3.1(react@18.3.1) - - react-i18next@15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3): + react-i18next@15.5.3(i18next@25.8.0(typescript@5.7.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.7.3): dependencies: '@babel/runtime': 7.27.6 html-parse-stringify: 3.0.1 i18next: 25.8.0(typescript@5.7.3) - react: 18.3.1 + react: 19.2.4 optionalDependencies: - react-dom: 18.3.1(react@18.3.1) + react-dom: 19.2.4(react@19.2.4) typescript: 5.7.3 - react-icons@5.5.0(react@18.3.1): + react-icons@5.5.0(react@19.2.4): dependencies: - react: 18.3.1 + react: 19.2.4 react-is@16.13.1: {} @@ -6869,18 +6900,16 @@ snapshots: react-is@18.3.1: {} - react-redux@9.2.0(@types/react@19.0.10)(react@18.3.1)(redux@5.0.1): + react-redux@9.2.0(@types/react@19.0.10)(react@19.2.4)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 - react: 18.3.1 - use-sync-external-store: 1.5.0(react@18.3.1) + react: 19.2.4 + use-sync-external-store: 1.5.0(react@19.2.4) optionalDependencies: '@types/react': 19.0.10 redux: 5.0.1 - react@18.3.1: - dependencies: - loose-envify: 1.4.0 + react@19.2.4: {} readable-stream@1.0.34: dependencies: @@ -6905,21 +6934,21 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - recharts@3.1.2(@types/react@19.0.10)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(redux@5.0.1): + recharts@3.1.2(@types/react@19.0.10)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1): dependencies: - '@reduxjs/toolkit': 2.9.0(react-redux@9.2.0(@types/react@19.0.10)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + '@reduxjs/toolkit': 2.9.0(react-redux@9.2.0(@types/react@19.0.10)(react@19.2.4)(redux@5.0.1))(react@19.2.4) clsx: 2.1.1 decimal.js-light: 2.5.1 es-toolkit: 1.39.10 eventemitter3: 5.0.1 immer: 10.1.3 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) react-is: 18.3.1 - react-redux: 9.2.0(@types/react@19.0.10)(react@18.3.1)(redux@5.0.1) + react-redux: 9.2.0(@types/react@19.0.10)(react@19.2.4)(redux@5.0.1) reselect: 5.1.1 tiny-invariant: 1.3.3 - use-sync-external-store: 1.5.0(react@18.3.1) + use-sync-external-store: 1.5.0(react@19.2.4) victory-vendor: 37.3.6 transitivePeerDependencies: - '@types/react' @@ -6947,8 +6976,6 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 - regenerator-runtime@0.14.1: {} - regexp.prototype.flags@1.5.4: dependencies: call-bind: 1.0.8 @@ -6960,6 +6987,8 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + reselect@5.1.1: {} resolve-alpn@1.2.1: {} @@ -7023,8 +7052,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.57.1 fsevents: 2.3.3 - rrweb-cssom@0.8.0: {} - run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -7064,9 +7091,7 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 + scheduler@0.27.0: {} seek-bzip@2.0.0: dependencies: @@ -7302,10 +7327,10 @@ snapshots: strnum@2.1.1: {} - styled-jsx@5.1.6(react@18.3.1): + styled-jsx@5.1.6(react@19.2.4): dependencies: client-only: 0.0.1 - react: 18.3.1 + react: 19.2.4 supports-color@7.2.0: dependencies: @@ -7313,11 +7338,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.4.0(react@18.3.1): + swr@2.4.0(react@19.2.4): dependencies: dequal: 2.0.3 - react: 18.3.1 - use-sync-external-store: 1.6.0(react@18.3.1) + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) symbol-tree@3.2.4: {} @@ -7329,9 +7354,9 @@ snapshots: tabbable@6.3.0: {} - tailwind-scrollbar@4.0.2(react@18.3.1)(tailwindcss@4.1.18): + tailwind-scrollbar@4.0.2(react@19.2.4)(tailwindcss@4.1.18): dependencies: - prism-react-renderer: 2.4.1(react@18.3.1) + prism-react-renderer: 2.4.1(react@19.2.4) tailwindcss: 4.1.18 transitivePeerDependencies: - react @@ -7399,14 +7424,8 @@ snapshots: tinyspy@4.0.4: {} - tldts-core@6.1.86: {} - tldts-core@7.0.12: {} - tldts@6.1.86: - dependencies: - tldts-core: 6.1.86 - tldts@7.0.12: dependencies: tldts-core: 7.0.12 @@ -7423,17 +7442,13 @@ snapshots: toidentifier@1.0.1: {} - tough-cookie@5.1.2: - dependencies: - tldts: 6.1.86 - tough-cookie@6.0.0: dependencies: tldts: 7.0.12 tr46@0.0.3: {} - tr46@5.1.1: + tr46@6.0.0: dependencies: punycode: 2.3.1 @@ -7504,6 +7519,8 @@ snapshots: undici-types@7.8.0: {} + undici@7.22.0: {} + unpipe@1.0.0: {} unrs-resolver@1.3.3: @@ -7532,13 +7549,13 @@ snapshots: dependencies: punycode: 2.3.1 - use-sync-external-store@1.5.0(react@18.3.1): + use-sync-external-store@1.5.0(react@19.2.4): dependencies: - react: 18.3.1 + react: 19.2.4 - use-sync-external-store@1.6.0(react@18.3.1): + use-sync-external-store@1.6.0(react@19.2.4): dependencies: - react: 18.3.1 + react: 19.2.4 util-deprecate@1.0.2: {} @@ -7598,7 +7615,7 @@ snapshots: jiti: 2.6.1 lightningcss: 1.30.2 - vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2): + vitest@3.2.4(@types/node@24.1.0)(jiti@2.6.1)(jsdom@28.1.0)(lightningcss@1.30.2): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -7625,7 +7642,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.1.0 - jsdom: 26.1.0 + jsdom: 28.1.0 transitivePeerDependencies: - jiti - less @@ -7648,18 +7665,17 @@ snapshots: webidl-conversions@3.0.1: {} - webidl-conversions@7.0.0: {} + webidl-conversions@8.0.1: {} - whatwg-encoding@3.1.1: + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: dependencies: - iconv-lite: 0.6.3 - - whatwg-mimetype@4.0.0: {} - - whatwg-url@14.2.0: - dependencies: - tr46: 5.1.1 - webidl-conversions: 7.0.0 + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' whatwg-url@5.0.0: dependencies: diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 08fed5656..66a6a34a7 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -184,6 +184,13 @@ "no_active": "No Active Streams", "plex_connection_error": "Check Plex Connection" }, + "tracearr": { + "no_active": "No Active Streams", + "streams": "Streams", + "transcodes": "Transcodes", + "directplay": "Direct Play", + "bitrate": "Bitrate" + }, "omada": { "connectedAp": "Connected APs", "activeUser": "Active devices", @@ -282,17 +289,13 @@ "approved": "Approved", "available": "Available" }, - "jellyseerr": { + "seerr": { "pending": "Pending", "approved": "Approved", "available": "Available", - "issues": "Open Issues" - }, - "overseerr": { - "pending": "Pending", + "completed": "Completed", "processing": "Processing", - "approved": "Approved", - "available": "Available" + "issues": "Open Issues" }, "netalertx": { "total": "Total", @@ -1171,5 +1174,11 @@ "paused": "Paused", "total": "Total", "environment_not_found": "Environment Not Found" + }, + "sparkyfitness": { + "eaten": "Eaten", + "burned": "Burned", + "remaining": "Remaining", + "steps": "Steps" } } diff --git a/src/__tests__/pages/api/services/proxy.test.js b/src/__tests__/pages/api/services/proxy.test.js index be35412b1..4d4390697 100644 --- a/src/__tests__/pages/api/services/proxy.test.js +++ b/src/__tests__/pages/api/services/proxy.test.js @@ -344,4 +344,17 @@ describe("pages/api/services/proxy", () => { expect(res.statusCode).toBe(500); expect(res.body).toEqual({ error: "Unexpected error" }); }); + + it("returns 500 when an async proxy handler throws", async () => { + getServiceWidget.mockResolvedValue({ type: "linkwarden" }); + handlerFn.handler.mockRejectedValueOnce(new Error("proxy boom")); + + const req = { method: "GET", query: { group: "g", service: "s", index: "0" } }; + const res = createMockRes(); + + await servicesProxy(req, res); + + expect(res.statusCode).toBe(500); + expect(res.body).toEqual({ error: "Unexpected error" }); + }); }); diff --git a/src/__tests__/pages/api/widgets/resources.test.js b/src/__tests__/pages/api/widgets/resources.test.js index 3ff5151ca..74506d0a4 100644 --- a/src/__tests__/pages/api/widgets/resources.test.js +++ b/src/__tests__/pages/api/widgets/resources.test.js @@ -90,17 +90,74 @@ describe("pages/api/widgets/resources", () => { }); it("returns 404 when requested network interface does not exist", async () => { - si.networkStats.mockResolvedValueOnce([{ iface: "en0" }]); + si.networkStats.mockResolvedValueOnce([{ iface: "en0" }]).mockResolvedValueOnce([ + { + iface: "missing", + operstate: "unknown", + rx_bytes: 0, + rx_dropped: 0, + rx_errors: 0, + tx_bytes: 0, + tx_dropped: 0, + tx_errors: 0, + rx_sec: null, + tx_sec: null, + ms: 0, + }, + ]); const req = { query: { type: "network", interfaceName: "missing" } }; const res = createMockRes(); await handler(req, res); + expect(si.networkStats).toHaveBeenNthCalledWith(1, "*"); + expect(si.networkStats).toHaveBeenNthCalledWith(2, "missing"); expect(res.statusCode).toBe(404); expect(res.body).toEqual({ error: "Interface not found" }); }); + it("falls back to direct named interface query when wildcard enumeration misses it", async () => { + si.networkStats.mockResolvedValueOnce([{ iface: "eth0", rx_bytes: 1 }]).mockResolvedValueOnce([ + { + iface: "eno1", + operstate: "up", + rx_bytes: 1000, + rx_dropped: 0, + rx_errors: 0, + tx_bytes: 500, + tx_dropped: 0, + tx_errors: 0, + rx_sec: null, + tx_sec: null, + ms: 0, + }, + ]); + + const req = { query: { type: "network", interfaceName: "eno1" } }; + const res = createMockRes(); + + await handler(req, res); + + expect(si.networkStats).toHaveBeenNthCalledWith(1, "*"); + expect(si.networkStats).toHaveBeenNthCalledWith(2, "eno1"); + expect(res.statusCode).toBe(200); + expect(res.body.interface).toBe("eno1"); + expect(res.body.network).toEqual({ + iface: "eno1", + operstate: "up", + rx_bytes: 1000, + rx_dropped: 0, + rx_errors: 0, + tx_bytes: 500, + tx_dropped: 0, + tx_errors: 0, + rx_sec: null, + tx_sec: null, + ms: 0, + }); + }); + it("returns default interface network stats", async () => { si.networkStats.mockResolvedValueOnce([{ iface: "en0", rx_bytes: 1 }]); si.networkInterfaceDefault.mockResolvedValueOnce("en0"); diff --git a/src/components/services/item.test.jsx b/src/components/services/item.test.jsx index ebd554fc1..a86811b5a 100644 --- a/src/components/services/item.test.jsx +++ b/src/components/services/item.test.jsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import { fireEvent, screen } from "@testing-library/react"; +import { act, fireEvent, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; @@ -188,7 +188,9 @@ describe("components/services/item", () => { // Still rendered while the close animation runs. expect(screen.getByTestId("docker-widget")).toBeInTheDocument(); - await vi.advanceTimersByTimeAsync(300); + act(() => { + vi.advanceTimersByTime(300); + }); expect(screen.queryByTestId("docker-widget")).not.toBeInTheDocument(); vi.useRealTimers(); diff --git a/src/components/services/widget/container.jsx b/src/components/services/widget/container.jsx index e5962e445..28ab4b330 100644 --- a/src/components/services/widget/container.jsx +++ b/src/components/services/widget/container.jsx @@ -9,6 +9,8 @@ import { buildHighlightConfig } from "utils/highlights"; const ALIASED_WIDGETS = { pialert: "netalertx", hoarder: "karakeep", + jellyseerr: "seerr", + overseerr: "seerr", }; export default function Container({ error = false, children, service }) { diff --git a/src/components/services/widget/container.test.jsx b/src/components/services/widget/container.test.jsx index e41118720..d1e731410 100644 --- a/src/components/services/widget/container.test.jsx +++ b/src/components/services/widget/container.test.jsx @@ -58,6 +58,26 @@ describe("components/services/widget/container", () => { expect(screen.getByTestId("karakeep.count")).toBeInTheDocument(); }); + it("supports seerr aliases when filtering (jellyseerr/overseerr -> seerr)", () => { + renderWithProviders( + + + , + { settings: {} }, + ); + + expect(screen.getByTestId("seerr.pending")).toBeInTheDocument(); + + renderWithProviders( + + + , + { settings: {} }, + ); + + expect(screen.getByTestId("seerr.processing")).toBeInTheDocument(); + }); + it("returns null when errors are hidden via settings.hideErrors", () => { const { container } = renderWithProviders( diff --git a/src/components/widgets/datetime/datetime.test.jsx b/src/components/widgets/datetime/datetime.test.jsx index da16f9dc6..f6035d3ad 100644 --- a/src/components/widgets/datetime/datetime.test.jsx +++ b/src/components/widgets/datetime/datetime.test.jsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import { screen } from "@testing-library/react"; +import { act, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; @@ -21,7 +21,9 @@ describe("components/widgets/datetime", () => { // `render` wraps in `act`, so effects should flush synchronously. expect(screen.getByText(expected0)).toBeInTheDocument(); - await vi.advanceTimersByTimeAsync(1000); + act(() => { + vi.advanceTimersByTime(1000); + }); const expected1 = new Intl.DateTimeFormat("en-US", format).format(new Date()); expect(screen.getByText(expected1)).toBeInTheDocument(); diff --git a/src/components/widgets/glances/glances.jsx b/src/components/widgets/glances/glances.jsx index 703b0e2de..7b1e27a57 100644 --- a/src/components/widgets/glances/glances.jsx +++ b/src/components/widgets/glances/glances.jsx @@ -11,7 +11,7 @@ import Resource from "../widget/resource"; import Resources from "../widget/resources"; import WidgetLabel from "../widget/widget_label"; -const cpuSensorLabels = ["cpu_thermal", "Core", "Tctl"]; +const cpuSensorLabels = ["cpu_thermal", "Core", "Tctl", "Temperature"]; function convertToFahrenheit(t) { return (t * 9) / 5 + 32; diff --git a/src/pages/api/services/proxy.js b/src/pages/api/services/proxy.js index 0cdf806ff..b6524972c 100644 --- a/src/pages/api/services/proxy.js +++ b/src/pages/api/services/proxy.js @@ -29,7 +29,7 @@ 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) { - return serviceProxyHandler(req, res); + return await serviceProxyHandler(req, res); } // map opaque endpoints to their actual endpoint @@ -90,15 +90,15 @@ export default async function handler(req, res) { } if (endpointProxy instanceof Function) { - return endpointProxy(req, res, map); + return await endpointProxy(req, res, map); } - return serviceProxyHandler(req, res, map); + return await serviceProxyHandler(req, res, map); } if (widget.allowedEndpoints instanceof RegExp) { if (widget.allowedEndpoints.test(req.query.endpoint)) { - return serviceProxyHandler(req, res); + return await serviceProxyHandler(req, res); } } diff --git a/src/pages/api/widgets/resources.js b/src/pages/api/widgets/resources.js index 19db010f5..812d3e7c6 100644 --- a/src/pages/api/widgets/resources.js +++ b/src/pages/api/widgets/resources.js @@ -4,6 +4,21 @@ import createLogger from "utils/logger"; const logger = createLogger("resources"); +function isMissingNetworkStat(networkData, interfaceName) { + return ( + networkData.operstate === "unknown" && + networkData.rx_bytes === 0 && + networkData.rx_dropped === 0 && + networkData.rx_errors === 0 && + networkData.tx_bytes === 0 && + networkData.tx_dropped === 0 && + networkData.tx_errors === 0 && + networkData.rx_sec === null && + networkData.tx_sec === null && + networkData.ms === 0 + ); +} + export default async function handler(req, res) { const { type, target, interfaceName = "default" } = req.query; @@ -64,6 +79,17 @@ export default async function handler(req, res) { logger.debug("networkData:", JSON.stringify(networkData)); if (interfaceName && interfaceName !== "default") { networkData = networkData.filter((network) => network.iface === interfaceName).at(0); + if (!networkData) { + // Fallback for e.g. docker where networkStats("*") may not return stats for host interfaces + const directNetworkData = await si.networkStats(interfaceName); + logger.debug("directNetworkData:", JSON.stringify(directNetworkData)); + networkData = Array.isArray(directNetworkData) ? directNetworkData.at(0) : null; + + // si returns unknown + zeroes when interface truly does not exist + if (!networkData || isMissingNetworkStat(networkData, interfaceName)) { + networkData = null; + } + } if (!networkData) { return res.status(404).json({ error: "Interface not found", diff --git a/src/test-utils/widget-assertions.js b/src/test-utils/widget-assertions.js index 67bd2ac44..bf9598c90 100644 --- a/src/test-utils/widget-assertions.js +++ b/src/test-utils/widget-assertions.js @@ -1,4 +1,12 @@ +import { expect } from "vitest"; + export function findServiceBlockByLabel(container, label) { const blocks = Array.from(container.querySelectorAll(".service-block")); return blocks.find((b) => b.textContent?.includes(label)); } + +export function expectBlockValue(container, label, value) { + const block = findServiceBlockByLabel(container, label); + expect(block, `missing block for ${label}`).toBeTruthy(); + expect(block.textContent).toContain(String(value)); +} diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index 0a2940a4f..52f8dcb05 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -313,7 +313,7 @@ export function cleanServiceGroups(groups) { enableNowPlaying, enableMediaControl, - // emby, jellyfin, tautulli + // emby, jellyfin, tautulli, tracearr enableUser, expandOneStreamToTwoRows, showEpisodeNumber, @@ -542,12 +542,15 @@ export function cleanServiceGroups(groups) { if (enableBlocks !== undefined) widget.enableBlocks = JSON.parse(enableBlocks); if (enableNowPlaying !== undefined) widget.enableNowPlaying = JSON.parse(enableNowPlaying); } - if (["emby", "jellyfin", "tautulli"].includes(type)) { + if (["emby", "jellyfin", "tautulli", "tracearr"].includes(type)) { if (expandOneStreamToTwoRows !== undefined) widget.expandOneStreamToTwoRows = !!JSON.parse(expandOneStreamToTwoRows); if (showEpisodeNumber !== undefined) widget.showEpisodeNumber = !!JSON.parse(showEpisodeNumber); if (enableUser !== undefined) widget.enableUser = !!JSON.parse(enableUser); } + if (type === "tracearr") { + if (view !== undefined) widget.view = view; + } if (["sonarr", "radarr"].includes(type)) { if (enableQueue !== undefined) widget.enableQueue = JSON.parse(enableQueue); } diff --git a/src/utils/config/service-helpers.test.js b/src/utils/config/service-helpers.test.js index 452b70f0b..0c6cf44ab 100644 --- a/src/utils/config/service-helpers.test.js +++ b/src/utils/config/service-helpers.test.js @@ -312,6 +312,13 @@ describe("utils/config/service-helpers", () => { { type: "healthchecks", uuid: "u" }, { type: "speedtest", bitratePrecision: "3", version: "1" }, { type: "stocks", watchlist: "AAPL", showUSMarketStatus: true }, + { + type: "tracearr", + expandOneStreamToTwoRows: "true", + showEpisodeNumber: "true", + enableUser: "true", + view: "both", + }, { type: "wgeasy", threshold: "10", version: "1" }, { type: "technitium", range: "24h" }, { type: "lubelogger", vehicleID: "12" }, @@ -350,6 +357,14 @@ describe("utils/config/service-helpers", () => { expect(widgets.find((w) => w.type === "speedtest")).toEqual( expect.objectContaining({ bitratePrecision: 3, version: 1 }), ); + expect(widgets.find((w) => w.type === "tracearr")).toEqual( + expect.objectContaining({ + expandOneStreamToTwoRows: true, + showEpisodeNumber: true, + enableUser: true, + view: "both", + }), + ); expect(widgets.find((w) => w.type === "jellystat")).toEqual(expect.objectContaining({ days: 7 })); expect(widgets.find((w) => w.type === "lubelogger")).toEqual(expect.objectContaining({ vehicleID: 12 })); }); diff --git a/src/utils/highlights.js b/src/utils/highlights.js index 65a3eeffd..a403ef1bd 100644 --- a/src/utils/highlights.js +++ b/src/utils/highlights.js @@ -74,6 +74,21 @@ const toNumber = (value) => { return undefined; }; +const extractNumericToken = (value) => { + if (typeof value !== "string") return undefined; + const match = value.match(/[-+]?\d[\d\s.,]*/); + if (!match) return undefined; + + const token = match[0].trim(); + if (!token) return undefined; + + const prefix = value.slice(0, match.index).trim(); + const suffix = value.slice((match.index ?? 0) + match[0].length).trim(); + if (/\d/.test(prefix) || /\d/.test(suffix)) return undefined; + + return token; +}; + const parseNumericValue = (value) => { if (value === null || value === undefined) return undefined; if (typeof value === "number" && Number.isFinite(value)) return value; @@ -85,7 +100,9 @@ const parseNumericValue = (value) => { const direct = Number(trimmed); if (!Number.isNaN(direct)) return direct; - const compact = trimmed.replace(/\s+/g, ""); + const candidate = extractNumericToken(trimmed); + const numericString = candidate ?? trimmed; + const compact = numericString.replace(/\s+/g, ""); if (!compact || !/^[-+]?[0-9.,]+$/.test(compact)) return undefined; const commaCount = (compact.match(/,/g) || []).length; diff --git a/src/utils/highlights.test.js b/src/utils/highlights.test.js index 9be6bd078..dca18a8e5 100644 --- a/src/utils/highlights.test.js +++ b/src/utils/highlights.test.js @@ -136,6 +136,9 @@ describe("utils/highlights", () => { const cfg = buildHighlightConfig(null, { // string numeric rule values go through toNumber() gt: { numeric: { when: "gt", value: "5", level: "warn" } }, + withUnitSuffix: { numeric: { when: "gt", value: 5, level: "warn" } }, + withUnitPrefix: { numeric: { when: "gt", value: 5, level: "warn" } }, + localizedUnitSuffix: { numeric: { when: "gt", value: 0.5, level: "warn" } }, commaGrouped: { numeric: { when: "eq", value: 1234, level: "good" } }, commaDecimal: { numeric: { when: "eq", value: 12.34, level: "good" } }, dotDecimal: { numeric: { when: "eq", value: 12.34, level: "good" } }, @@ -143,6 +146,12 @@ describe("utils/highlights", () => { }); expect(evaluateHighlight("gt", "6", cfg)).toMatchObject({ level: "warn", source: "numeric" }); + expect(evaluateHighlight("withUnitSuffix", "5.2 ms", cfg)).toMatchObject({ level: "warn", source: "numeric" }); + expect(evaluateHighlight("withUnitPrefix", "ms 5.2", cfg)).toMatchObject({ level: "warn", source: "numeric" }); + expect(evaluateHighlight("localizedUnitSuffix", "0,71\u202Fms", cfg)).toMatchObject({ + level: "warn", + source: "numeric", + }); expect(evaluateHighlight("commaGrouped", "1,234", cfg)).toMatchObject({ level: "good", source: "numeric" }); expect(evaluateHighlight("commaDecimal", "12,34", cfg)).toMatchObject({ level: "good", source: "numeric" }); // Include a space so Number(trimmed) fails and we exercise the dot parsing branch. @@ -161,6 +170,9 @@ describe("utils/highlights", () => { // "1.2.3" is not a valid grouped or decimal number for our parser. expect(evaluateHighlight("num", "1.2.3", cfg)).toBeNull(); + // Multiple numbers in one string should not be treated as a single numeric value. + expect(evaluateHighlight("num", "5/10 ms", cfg)).toBeNull(); + // JSX-ish values should not be treated as numeric. expect(evaluateHighlight("num", { props: { children: "x" } }, cfg)).toBeNull(); }); diff --git a/src/utils/proxy/handlers/credentialed.js b/src/utils/proxy/handlers/credentialed.js index 4a767b55c..27f02f3e5 100644 --- a/src/utils/proxy/handlers/credentialed.js +++ b/src/utils/proxy/handlers/credentialed.js @@ -64,6 +64,7 @@ export default async function credentialedProxyHandler(req, res, map) { "pangolin", "tailscale", "tandoor", + "tracearr", "pterodactyl", "vikunja", "firefly", diff --git a/src/widgets/arcane/component.test.jsx b/src/widgets/arcane/component.test.jsx index cb6fe6a5e..5e6a78587 100644 --- a/src/widgets/arcane/component.test.jsx +++ b/src/widgets/arcane/component.test.jsx @@ -4,6 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ import Component from "./component"; -function expectBlockValue(container, label, value) { - const blocks = Array.from(container.querySelectorAll(".service-block")); - const block = blocks.find((b) => b.textContent?.includes(label)); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/arcane/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/argocd/component.test.jsx b/src/widgets/argocd/component.test.jsx index e59175cf2..88b102643 100644 --- a/src/widgets/argocd/component.test.jsx +++ b/src/widgets/argocd/component.test.jsx @@ -4,6 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ import Component from "./component"; -function expectBlockValue(container, label, value) { - const blocks = Array.from(container.querySelectorAll(".service-block")); - const block = blocks.find((b) => b.textContent?.includes(label)); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/argocd/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/audiobookshelf/component.test.jsx b/src/widgets/audiobookshelf/component.test.jsx index 9f74a89d2..449ae0703 100644 --- a/src/widgets/audiobookshelf/component.test.jsx +++ b/src/widgets/audiobookshelf/component.test.jsx @@ -4,6 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ import Component from "./component"; -function expectBlockValue(container, label, value) { - const blocks = Array.from(container.querySelectorAll(".service-block")); - const block = blocks.find((b) => b.textContent?.includes(label)); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/audiobookshelf/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/authentik/component.test.jsx b/src/widgets/authentik/component.test.jsx index 43a4f39c4..4b1688391 100644 --- a/src/widgets/authentik/component.test.jsx +++ b/src/widgets/authentik/component.test.jsx @@ -4,6 +4,7 @@ import { screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ import Component from "./component"; -function expectBlockValue(container, label, value) { - const blocks = Array.from(container.querySelectorAll(".service-block")); - const block = blocks.find((b) => b.textContent?.includes(label)); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/authentik/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/autobrr/component.test.jsx b/src/widgets/autobrr/component.test.jsx index 8f828b2eb..4607bf51a 100644 --- a/src/widgets/autobrr/component.test.jsx +++ b/src/widgets/autobrr/component.test.jsx @@ -4,6 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ import Component from "./component"; -function expectBlockValue(container, label, value) { - const blocks = Array.from(container.querySelectorAll(".service-block")); - const block = blocks.find((b) => b.textContent?.includes(label)); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/autobrr/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/azuredevops/component.test.jsx b/src/widgets/azuredevops/component.test.jsx index e540e85d7..ae42730f4 100644 --- a/src/widgets/azuredevops/component.test.jsx +++ b/src/widgets/azuredevops/component.test.jsx @@ -4,6 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -13,13 +14,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ import Component from "./component"; -function expectBlockValue(container, label, value) { - const blocks = Array.from(container.querySelectorAll(".service-block")); - const block = blocks.find((b) => b.textContent?.includes(label)); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/azuredevops/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/beszel/component.jsx b/src/widgets/beszel/component.jsx index e80a9fabe..bbcaeb0b7 100644 --- a/src/widgets/beszel/component.jsx +++ b/src/widgets/beszel/component.jsx @@ -54,7 +54,10 @@ export default function Component({ service }) { - + ); } diff --git a/src/widgets/beszel/component.test.jsx b/src/widgets/beszel/component.test.jsx index ee6218f2d..ba3d0c618 100644 --- a/src/widgets/beszel/component.test.jsx +++ b/src/widgets/beszel/component.test.jsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/beszel/component", () => { beforeEach(() => { vi.clearAllMocks(); @@ -82,6 +76,35 @@ describe("widgets/beszel/component", () => { expect(screen.queryByText("beszel.updated")).toBeNull(); }); + it("renders optional fields", () => { + useWidgetAPI.mockReturnValue({ + data: { + totalItems: 1, + items: [ + { + id: "sys1", + name: "MySystem", + status: "up", + updated: 123, + info: { cpu: 10, mp: 20, dp: 30, b: 40, bb: 14.5 }, + }, + ], + }, + error: undefined, + }); + + const service = { + widget: { type: "beszel", systemId: "sys1", fields: ["name", "disk", "network"] }, + }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(["name", "disk", "network"]); + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expectBlockValue(container, "beszel.name", "MySystem"); + expectBlockValue(container, "beszel.disk", 30); + expectBlockValue(container, "beszel.network", 14.5); + }); + it("renders error when systemId is not found", () => { useWidgetAPI.mockReturnValue({ data: { totalItems: 1, items: [{ id: "sys1", name: "MySystem", status: "up", info: {} }] }, diff --git a/src/widgets/caddy/component.test.jsx b/src/widgets/caddy/component.test.jsx index 43a060455..01952a9fc 100644 --- a/src/widgets/caddy/component.test.jsx +++ b/src/widgets/caddy/component.test.jsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/caddy/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/changedetectionio/component.test.jsx b/src/widgets/changedetectionio/component.test.jsx index ef777d16c..b227a07bd 100644 --- a/src/widgets/changedetectionio/component.test.jsx +++ b/src/widgets/changedetectionio/component.test.jsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/changedetectionio/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/channelsdvrserver/component.test.jsx b/src/widgets/channelsdvrserver/component.test.jsx index 5198999e0..0ff4fe72e 100644 --- a/src/widgets/channelsdvrserver/component.test.jsx +++ b/src/widgets/channelsdvrserver/component.test.jsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/channelsdvrserver/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/checkmk/component.test.jsx b/src/widgets/checkmk/component.test.jsx index 25e5e529b..7f00069fa 100644 --- a/src/widgets/checkmk/component.test.jsx +++ b/src/widgets/checkmk/component.test.jsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/checkmk/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/cloudflared/component.test.jsx b/src/widgets/cloudflared/component.test.jsx index e6904536a..9a81a0835 100644 --- a/src/widgets/cloudflared/component.test.jsx +++ b/src/widgets/cloudflared/component.test.jsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/cloudflared/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/components.js b/src/widgets/components.js index 61585f5f6..472ddd684 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -65,7 +65,7 @@ const components = { jackett: dynamic(() => import("./jackett/component")), jdownloader: dynamic(() => import("./jdownloader/component")), jellyfin: dynamic(() => import("./jellyfin/component")), - jellyseerr: dynamic(() => import("./jellyseerr/component")), + jellyseerr: dynamic(() => import("./seerr/component")), jellystat: dynamic(() => import("./jellystat/component")), kavita: dynamic(() => import("./kavita/component")), komga: dynamic(() => import("./komga/component")), @@ -97,7 +97,7 @@ const components = { ombi: dynamic(() => import("./ombi/component")), opendtu: dynamic(() => import("./opendtu/component")), opnsense: dynamic(() => import("./opnsense/component")), - overseerr: dynamic(() => import("./overseerr/component")), + overseerr: dynamic(() => import("./seerr/component")), openmediavault: dynamic(() => import("./openmediavault/component")), openwrt: dynamic(() => import("./openwrt/component")), paperlessngx: dynamic(() => import("./paperlessngx/component")), @@ -124,8 +124,10 @@ const components = { rutorrent: dynamic(() => import("./rutorrent/component")), sabnzbd: dynamic(() => import("./sabnzbd/component")), scrutiny: dynamic(() => import("./scrutiny/component")), + seerr: dynamic(() => import("./seerr/component")), slskd: dynamic(() => import("./slskd/component")), sonarr: dynamic(() => import("./sonarr/component")), + sparkyfitness: dynamic(() => import("./sparkyfitness/component")), speedtest: dynamic(() => import("./speedtest/component")), spoolman: dynamic(() => import("./spoolman/component")), stash: dynamic(() => import("./stash/component")), @@ -138,6 +140,7 @@ const components = { tautulli: dynamic(() => import("./tautulli/component")), technitium: dynamic(() => import("./technitium/component")), tdarr: dynamic(() => import("./tdarr/component")), + tracearr: dynamic(() => import("./tracearr/component")), traefik: dynamic(() => import("./traefik/component")), transmission: dynamic(() => import("./transmission/component")), trilium: dynamic(() => import("./trilium/component")), diff --git a/src/widgets/crowdsec/component.test.jsx b/src/widgets/crowdsec/component.test.jsx index 4c2d4f2b7..abc4c8696 100644 --- a/src/widgets/crowdsec/component.test.jsx +++ b/src/widgets/crowdsec/component.test.jsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -12,12 +12,6 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/crowdsec/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/crowdsec/proxy.js b/src/widgets/crowdsec/proxy.js index d3257fa6f..9d41e6b2a 100644 --- a/src/widgets/crowdsec/proxy.js +++ b/src/widgets/crowdsec/proxy.js @@ -25,13 +25,25 @@ async function login(widget, service) { }), }); - const dataParsed = JSON.parse(data); + let dataParsed; + try { + dataParsed = JSON.parse(data); + } catch { + logger.error("Failed to parse Crowdsec login response, status: %d", status); + cache.del(`${sessionTokenCacheKey}.${service}`); + return null; + } - if (!(status === 200) || !dataParsed.token) { + if (status !== 200 || !dataParsed.token) { logger.error("Failed to login to Crowdsec API, status: %d", status); cache.del(`${sessionTokenCacheKey}.${service}`); + return null; } - cache.put(`${sessionTokenCacheKey}.${service}`, dataParsed.token, new Date(dataParsed.expire) - new Date()); + + const ttl = Math.max(new Date(dataParsed.expire) - new Date(), 1); + cache.put(`${sessionTokenCacheKey}.${service}`, dataParsed.token, ttl); + + return dataParsed.token; } export default async function crowdsecProxyHandler(req, res) { @@ -48,11 +60,10 @@ export default async function crowdsecProxyHandler(req, res) { return res.status(400).json({ error: "Invalid widget configuration" }); } - if (!cache.get(`${sessionTokenCacheKey}.${service}`)) { - await login(widget, service); + let token = cache.get(`${sessionTokenCacheKey}.${service}`); + if (!token) { + token = await login(widget, service); } - - const token = cache.get(`${sessionTokenCacheKey}.${service}`); if (!token) { return res.status(500).json({ error: "Failed to authenticate with Crowdsec" }); } @@ -71,7 +82,20 @@ export default async function crowdsecProxyHandler(req, res) { logger.debug("Calling Crowdsec API endpoint: %s", endpoint); - const [status, , data] = await httpProxy(url, params); + let [status, , data] = await httpProxy(url, params); + + if (status === 401) { + logger.debug("Crowdsec API returned 401, refreshing token and retrying request"); + cache.del(`${sessionTokenCacheKey}.${service}`); + const refreshedToken = await login(widget, service); + + if (!refreshedToken) { + return res.status(500).json({ error: "Failed to authenticate with Crowdsec" }); + } + + params.headers.Authorization = `Bearer ${refreshedToken}`; + [status, , data] = await httpProxy(url, params); + } if (status !== 200) { logger.error("Error calling Crowdsec API: %d. Data: %s", status, data); diff --git a/src/widgets/crowdsec/proxy.test.js b/src/widgets/crowdsec/proxy.test.js index 7555cf16a..157a96a53 100644 --- a/src/widgets/crowdsec/proxy.test.js +++ b/src/widgets/crowdsec/proxy.test.js @@ -89,4 +89,76 @@ describe("widgets/crowdsec/proxy", () => { expect(res.statusCode).toBe(500); expect(res.body).toEqual({ error: "Failed to authenticate with Crowdsec" }); }); + + it("re-authenticates and retries once when API returns 401", async () => { + getServiceWidget.mockResolvedValue({ + type: "crowdsec", + url: "http://cs", + username: "machine", + password: "pw", + }); + + httpProxy + .mockResolvedValueOnce([ + 200, + "application/json", + JSON.stringify({ token: "tok-old", expire: new Date(Date.now() + 60_000).toISOString() }), + ]) + .mockResolvedValueOnce([401, "application/json", Buffer.from("bad token")]) + .mockResolvedValueOnce([ + 200, + "application/json", + JSON.stringify({ token: "tok-new", expire: new Date(Date.now() + 60_000).toISOString() }), + ]) + .mockResolvedValueOnce([200, "application/json", Buffer.from("data")]); + + const req = { query: { group: "g", service: "svc", endpoint: "alerts", index: "0" } }; + const res = createMockRes(); + + await crowdsecProxyHandler(req, res); + + expect(httpProxy).toHaveBeenCalledTimes(4); + expect(httpProxy.mock.calls[3][1].headers.Authorization).toBe("Bearer tok-new"); + expect(res.statusCode).toBe(200); + expect(res.body).toEqual(Buffer.from("data")); + }); + + it("returns 500 when 401 refresh fails to get a new token", async () => { + getServiceWidget.mockResolvedValue({ + type: "crowdsec", + url: "http://cs", + username: "machine", + password: "pw", + }); + + httpProxy + .mockResolvedValueOnce([ + 200, + "application/json", + JSON.stringify({ token: "tok-old", expire: new Date(Date.now() + 60_000).toISOString() }), + ]) + .mockResolvedValueOnce([401, "application/json", Buffer.from("bad token")]) + .mockResolvedValueOnce([500, "application/json", JSON.stringify({ error: "no token" })]); + + const req = { query: { group: "g", service: "svc", endpoint: "alerts", index: "0" } }; + const res = createMockRes(); + + await crowdsecProxyHandler(req, res); + + expect(res.statusCode).toBe(500); + expect(res.body).toEqual({ error: "Failed to authenticate with Crowdsec" }); + }); + + it("returns 500 when login response is not JSON", async () => { + getServiceWidget.mockResolvedValue({ type: "crowdsec", url: "http://cs", username: "machine", password: "pw" }); + httpProxy.mockResolvedValueOnce([200, "text/plain", "not-json"]); + + const req = { query: { group: "g", service: "svc", endpoint: "alerts", index: "0" } }; + const res = createMockRes(); + + await crowdsecProxyHandler(req, res); + + expect(res.statusCode).toBe(500); + expect(res.body).toEqual({ error: "Failed to authenticate with Crowdsec" }); + }); }); diff --git a/src/widgets/deluge/component.test.jsx b/src/widgets/deluge/component.test.jsx index bdda50f02..677449883 100644 --- a/src/widgets/deluge/component.test.jsx +++ b/src/widgets/deluge/component.test.jsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); @@ -16,12 +16,6 @@ vi.mock("../../components/widgets/queue/queueEntry", () => ({ import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/deluge/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/diskstation/component.test.jsx b/src/widgets/diskstation/component.test.jsx index 449c4a619..dc1153fb0 100644 --- a/src/widgets/diskstation/component.test.jsx +++ b/src/widgets/diskstation/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/diskstation/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/dispatcharr/component.test.jsx b/src/widgets/dispatcharr/component.test.jsx index 85f237b33..4a9257811 100644 --- a/src/widgets/dispatcharr/component.test.jsx +++ b/src/widgets/dispatcharr/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/dispatcharr/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/dockhand/component.test.jsx b/src/widgets/dockhand/component.test.jsx index f3ca02fc4..d9f0be490 100644 --- a/src/widgets/dockhand/component.test.jsx +++ b/src/widgets/dockhand/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/dockhand/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/downloadstation/component.test.jsx b/src/widgets/downloadstation/component.test.jsx index 5e25f7e3e..5dd529f63 100644 --- a/src/widgets/downloadstation/component.test.jsx +++ b/src/widgets/downloadstation/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/downloadstation/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/esphome/component.test.jsx b/src/widgets/esphome/component.test.jsx index 41991c1dc..10ad52176 100644 --- a/src/widgets/esphome/component.test.jsx +++ b/src/widgets/esphome/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/esphome/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/evcc/component.test.jsx b/src/widgets/evcc/component.test.jsx index a337deb6f..7922a0135 100644 --- a/src/widgets/evcc/component.test.jsx +++ b/src/widgets/evcc/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/evcc/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/filebrowser/component.test.jsx b/src/widgets/filebrowser/component.test.jsx index 7eb0e12b7..be31633ae 100644 --- a/src/widgets/filebrowser/component.test.jsx +++ b/src/widgets/filebrowser/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/filebrowser/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/fileflows/component.test.jsx b/src/widgets/fileflows/component.test.jsx index 74dd5deec..5b0e42771 100644 --- a/src/widgets/fileflows/component.test.jsx +++ b/src/widgets/fileflows/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/fileflows/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/flood/component.test.jsx b/src/widgets/flood/component.test.jsx index ad49bcfbd..fd53ffecb 100644 --- a/src/widgets/flood/component.test.jsx +++ b/src/widgets/flood/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/flood/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/freshrss/component.test.jsx b/src/widgets/freshrss/component.test.jsx index e5dd20033..c4685de3d 100644 --- a/src/widgets/freshrss/component.test.jsx +++ b/src/widgets/freshrss/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/freshrss/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/frigate/component.test.jsx b/src/widgets/frigate/component.test.jsx index f9805a6df..5f0cfaed7 100644 --- a/src/widgets/frigate/component.test.jsx +++ b/src/widgets/frigate/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/frigate/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/fritzbox/component.test.jsx b/src/widgets/fritzbox/component.test.jsx index 04a7e268e..f1abcaf41 100644 --- a/src/widgets/fritzbox/component.test.jsx +++ b/src/widgets/fritzbox/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +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"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/fritzbox/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/gamedig/component.test.jsx b/src/widgets/gamedig/component.test.jsx index d56b93bfa..895b4d3bc 100644 --- a/src/widgets/gamedig/component.test.jsx +++ b/src/widgets/gamedig/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/gamedig/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/gatus/component.test.jsx b/src/widgets/gatus/component.test.jsx index 0d5b144a8..51176baad 100644 --- a/src/widgets/gatus/component.test.jsx +++ b/src/widgets/gatus/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/gatus/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/ghostfolio/component.test.jsx b/src/widgets/ghostfolio/component.test.jsx index d71cd5b36..05277071a 100644 --- a/src/widgets/ghostfolio/component.test.jsx +++ b/src/widgets/ghostfolio/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/ghostfolio/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/gitea/component.test.jsx b/src/widgets/gitea/component.test.jsx index c54267677..c0325bba1 100644 --- a/src/widgets/gitea/component.test.jsx +++ b/src/widgets/gitea/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/gitea/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/gitlab/component.test.jsx b/src/widgets/gitlab/component.test.jsx index f19cbf3bc..4fa741519 100644 --- a/src/widgets/gitlab/component.test.jsx +++ b/src/widgets/gitlab/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/gitlab/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/glances/metrics/containers.jsx b/src/widgets/glances/metrics/containers.jsx index 93ecbc28f..8968305a8 100644 --- a/src/widgets/glances/metrics/containers.jsx +++ b/src/widgets/glances/metrics/containers.jsx @@ -8,7 +8,9 @@ import useWidgetAPI from "utils/proxy/use-widget-api"; const statusMap = { running: , + healthy: , paused: , + stopped: , }; const defaultInterval = 1000; diff --git a/src/widgets/glances/metrics/containers.test.jsx b/src/widgets/glances/metrics/containers.test.jsx index b3a8dbeb4..c7c8a71bf 100644 --- a/src/widgets/glances/metrics/containers.test.jsx +++ b/src/widgets/glances/metrics/containers.test.jsx @@ -11,6 +11,15 @@ vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); // Avoid pulling Next/Image + ThemeContext requirements into these unit tests. vi.mock("components/resolvedicon", () => ({ default: () => })); +vi.mock("next-i18next", () => ({ + useTranslation: () => ({ + t: (key, opts) => (key === "common.bytes" ? `${key}:${opts?.value}` : key), + }), +})); + +// Avoid pulling Next/Image + ThemeContext requirements into these unit tests. +vi.mock("components/resolvedicon", () => ({ default: () => })); + import Component from "./containers"; describe("widgets/glances/metrics/containers", () => { @@ -21,4 +30,78 @@ describe("widgets/glances/metrics/containers", () => { }); expect(screen.getByText("-")).toBeInTheDocument(); }); + + it("renders a placeholder while loading", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + renderWithProviders(, { + settings: { hideErrors: false }, + }); + expect(screen.getByText("-")).toBeInTheDocument(); + }); + + it("renders nothing when there is an error", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: new Error("fail") }); + renderWithProviders(, { + settings: { hideErrors: false }, + }); + expect(screen.queryByText("resources.cpu")).not.toBeInTheDocument(); + expect(screen.queryByText("-")).not.toBeInTheDocument(); + }); + + it("renders container rows using v3 keys and formats values", () => { + useWidgetAPI.mockReturnValue({ + data: [ + { + Id: "one", + Status: "running", + name: "alpha", + cpu_percent: 12.34, + memory: { usage: 1000, inactive_file: 400 }, + }, + { + Id: "two", + Status: "paused", + name: "beta", + cpu_percent: 99.99, + memory: { usage: 2000, inactive_file: 1000 }, + }, + ], + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + // data.splice(1) keeps only one item when chart is false + expect(screen.getByText("resources.cpu")).toBeInTheDocument(); + expect(screen.getByText("resources.mem")).toBeInTheDocument(); + + expect(screen.getByText("alpha")).toBeInTheDocument(); + expect(screen.queryByText("beta")).not.toBeInTheDocument(); + + expect(screen.getByText("12.3%")).toBeInTheDocument(); + expect(screen.getByText("common.bytes:600")).toBeInTheDocument(); + expect(screen.getAllByTestId("resolvedicon")).toHaveLength(1); + }); + + it("limits rows to 5 when chart is enabled", () => { + const data = Array.from({ length: 6 }).map((_, index) => ({ + Id: `id-${index}`, + Status: "healthy", + name: `item-${index}`, + cpu_percent: index + 0.1, + memory: { usage: 100 * (index + 1), inactive_file: 0 }, + })); + + useWidgetAPI.mockReturnValue({ data, error: undefined }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("item-0")).toBeInTheDocument(); + expect(screen.getByText("item-4")).toBeInTheDocument(); + expect(screen.queryByText("item-5")).not.toBeInTheDocument(); + }); }); diff --git a/src/widgets/glances/metrics/process.jsx b/src/widgets/glances/metrics/process.jsx index 333c8a344..a7eedde1c 100644 --- a/src/widgets/glances/metrics/process.jsx +++ b/src/widgets/glances/metrics/process.jsx @@ -46,7 +46,7 @@ export default function Component({ service }) { let listYPosition = "bottom-4"; if (chart) { headerYPosition = "-top-6"; - listYPosition = "-top-3"; + listYPosition = "-top-2"; } return ( diff --git a/src/widgets/gluetun/component.test.jsx b/src/widgets/gluetun/component.test.jsx index 0e6c1b35a..aeb617493 100644 --- a/src/widgets/gluetun/component.test.jsx +++ b/src/widgets/gluetun/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/gluetun/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/gotify/component.test.jsx b/src/widgets/gotify/component.test.jsx index b83f7cc4e..853c627f2 100644 --- a/src/widgets/gotify/component.test.jsx +++ b/src/widgets/gotify/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/gotify/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/grafana/component.test.jsx b/src/widgets/grafana/component.test.jsx index 30e39c1ea..ab30f90c0 100644 --- a/src/widgets/grafana/component.test.jsx +++ b/src/widgets/grafana/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/grafana/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/hdhomerun/component.test.jsx b/src/widgets/hdhomerun/component.test.jsx index 7f4ef5452..b0afac7b4 100644 --- a/src/widgets/hdhomerun/component.test.jsx +++ b/src/widgets/hdhomerun/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/hdhomerun/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/headscale/component.test.jsx b/src/widgets/headscale/component.test.jsx index 39715ec1f..d4c321556 100644 --- a/src/widgets/headscale/component.test.jsx +++ b/src/widgets/headscale/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/headscale/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/healthchecks/component.test.jsx b/src/widgets/healthchecks/component.test.jsx index 92a11f105..ed1a15a59 100644 --- a/src/widgets/healthchecks/component.test.jsx +++ b/src/widgets/healthchecks/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/healthchecks/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/homebox/component.test.jsx b/src/widgets/homebox/component.test.jsx index 164247d73..0342e8392 100644 --- a/src/widgets/homebox/component.test.jsx +++ b/src/widgets/homebox/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +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"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/homebox/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/immich/component.test.jsx b/src/widgets/immich/component.test.jsx index fa282ee05..2149d21d9 100644 --- a/src/widgets/immich/component.test.jsx +++ b/src/widgets/immich/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/immich/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/jackett/component.test.jsx b/src/widgets/jackett/component.test.jsx index 99cb4ce02..a3ee09cde 100644 --- a/src/widgets/jackett/component.test.jsx +++ b/src/widgets/jackett/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/jackett/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/jdownloader/component.test.jsx b/src/widgets/jdownloader/component.test.jsx index 1a3fab6ff..a83ea1991 100644 --- a/src/widgets/jdownloader/component.test.jsx +++ b/src/widgets/jdownloader/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/jdownloader/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/jellyseerr/component.jsx b/src/widgets/jellyseerr/component.jsx deleted file mode 100644 index d405cbf62..000000000 --- a/src/widgets/jellyseerr/component.jsx +++ /dev/null @@ -1,39 +0,0 @@ -import Block from "components/services/widget/block"; -import Container from "components/services/widget/container"; - -import useWidgetAPI from "utils/proxy/use-widget-api"; - -export const jellyseerrDefaultFields = ["pending", "approved", "available"]; - -export default function Component({ service }) { - const { widget } = service; - - widget.fields = widget?.fields?.length ? widget.fields : jellyseerrDefaultFields; - const isIssueEnabled = widget.fields.includes("issues"); - - const { data: statsData, error: statsError } = useWidgetAPI(widget, "request/count"); - const { data: issueData, error: issueError } = useWidgetAPI(widget, isIssueEnabled ? "issue/count" : ""); - if (statsError || (isIssueEnabled && issueError)) { - return ; - } - - if (!statsData || (isIssueEnabled && !issueData)) { - return ( - - - - - - - ); - } - - return ( - - - - - - - ); -} diff --git a/src/widgets/jellyseerr/component.test.jsx b/src/widgets/jellyseerr/component.test.jsx deleted file mode 100644 index 16a3fccfb..000000000 --- a/src/widgets/jellyseerr/component.test.jsx +++ /dev/null @@ -1,63 +0,0 @@ -// @vitest-environment jsdom - -import { screen } from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; - -import { renderWithProviders } from "test-utils/render-with-providers"; - -const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); -vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); - -import Component, { jellyseerrDefaultFields } from "./component"; - -describe("widgets/jellyseerr/component", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it("defaults fields and filters to 3 blocks while loading when issues are not enabled", () => { - useWidgetAPI - .mockReturnValueOnce({ data: undefined, error: undefined }) // request/count - .mockReturnValueOnce({ data: undefined, error: undefined }); // issue/count disabled (endpoint = "") - - const service = { widget: { type: "jellyseerr", url: "http://x" } }; - const { container } = renderWithProviders(, { settings: { hideErrors: false } }); - - expect(service.widget.fields).toEqual(jellyseerrDefaultFields); - expect(useWidgetAPI.mock.calls[1][1]).toBe(""); - expect(container.querySelectorAll(".service-block")).toHaveLength(3); - expect(screen.getByText("jellyseerr.pending")).toBeInTheDocument(); - expect(screen.getByText("jellyseerr.approved")).toBeInTheDocument(); - expect(screen.getByText("jellyseerr.available")).toBeInTheDocument(); - expect(screen.queryByText("jellyseerr.issues")).toBeNull(); - }); - - it("renders issues when enabled (and calls the issue/count endpoint)", () => { - useWidgetAPI - .mockReturnValueOnce({ data: { pending: 1, approved: 2, available: 3 }, error: undefined }) - .mockReturnValueOnce({ data: { open: 1, total: 2 }, error: undefined }); - - const service = { - widget: { type: "jellyseerr", url: "http://x", fields: ["pending", "approved", "available", "issues"] }, - }; - const { container } = renderWithProviders(, { settings: { hideErrors: false } }); - - expect(useWidgetAPI.mock.calls[1][1]).toBe("issue/count"); - expect(container.querySelectorAll(".service-block")).toHaveLength(4); - expect(screen.getByText("1 / 2")).toBeInTheDocument(); - }); - - it("renders error UI when issues are enabled and issue/count errors", () => { - useWidgetAPI - .mockReturnValueOnce({ data: { pending: 0, approved: 0, available: 0 }, error: undefined }) - .mockReturnValueOnce({ data: undefined, error: { message: "nope" } }); - - renderWithProviders( - , - { settings: { hideErrors: false } }, - ); - - expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0); - expect(screen.getByText("nope")).toBeInTheDocument(); - }); -}); diff --git a/src/widgets/karakeep/component.test.jsx b/src/widgets/karakeep/component.test.jsx index bf6d7ca2c..6b95e71e8 100644 --- a/src/widgets/karakeep/component.test.jsx +++ b/src/widgets/karakeep/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +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"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/karakeep/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/komga/component.test.jsx b/src/widgets/komga/component.test.jsx index 4459b21bc..a2a6a95f3 100644 --- a/src/widgets/komga/component.test.jsx +++ b/src/widgets/komga/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/komga/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/kopia/component.test.jsx b/src/widgets/kopia/component.test.jsx index 96cd4cb36..98af11a13 100644 --- a/src/widgets/kopia/component.test.jsx +++ b/src/widgets/kopia/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/kopia/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/lidarr/component.test.jsx b/src/widgets/lidarr/component.test.jsx index ecbacfd38..106371433 100644 --- a/src/widgets/lidarr/component.test.jsx +++ b/src/widgets/lidarr/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/lidarr/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/linkwarden/component.test.jsx b/src/widgets/linkwarden/component.test.jsx index 5cb0ba6c5..a87a7975b 100644 --- a/src/widgets/linkwarden/component.test.jsx +++ b/src/widgets/linkwarden/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/linkwarden/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/lubelogger/component.test.jsx b/src/widgets/lubelogger/component.test.jsx index 2890a0f88..da81ff095 100644 --- a/src/widgets/lubelogger/component.test.jsx +++ b/src/widgets/lubelogger/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/lubelogger/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/mailcow/component.test.jsx b/src/widgets/mailcow/component.test.jsx index adc5794fb..a679de650 100644 --- a/src/widgets/mailcow/component.test.jsx +++ b/src/widgets/mailcow/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/mailcow/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/mastodon/component.test.jsx b/src/widgets/mastodon/component.test.jsx index 589dc6fe1..d50b48935 100644 --- a/src/widgets/mastodon/component.test.jsx +++ b/src/widgets/mastodon/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/mastodon/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/mealie/component.test.jsx b/src/widgets/mealie/component.test.jsx index 7cb2fe660..ade5a9b9a 100644 --- a/src/widgets/mealie/component.test.jsx +++ b/src/widgets/mealie/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/mealie/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/medusa/component.test.jsx b/src/widgets/medusa/component.test.jsx index ae6bfa752..9ab501b22 100644 --- a/src/widgets/medusa/component.test.jsx +++ b/src/widgets/medusa/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/medusa/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/mikrotik/component.test.jsx b/src/widgets/mikrotik/component.test.jsx index 87f4af44c..4697d9dd7 100644 --- a/src/widgets/mikrotik/component.test.jsx +++ b/src/widgets/mikrotik/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/mikrotik/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/minecraft/component.test.jsx b/src/widgets/minecraft/component.test.jsx index 399056232..a26c677b4 100644 --- a/src/widgets/minecraft/component.test.jsx +++ b/src/widgets/minecraft/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/minecraft/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/miniflux/component.test.jsx b/src/widgets/miniflux/component.test.jsx index 0a3ec4d1c..938147123 100644 --- a/src/widgets/miniflux/component.test.jsx +++ b/src/widgets/miniflux/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/miniflux/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/moonraker/component.test.jsx b/src/widgets/moonraker/component.test.jsx index dab81e317..f99a5bb74 100644 --- a/src/widgets/moonraker/component.test.jsx +++ b/src/widgets/moonraker/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/moonraker/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/mylar/component.test.jsx b/src/widgets/mylar/component.test.jsx index 4e7846525..d2464c1a5 100644 --- a/src/widgets/mylar/component.test.jsx +++ b/src/widgets/mylar/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/mylar/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/myspeed/component.test.jsx b/src/widgets/myspeed/component.test.jsx index 5bc5fb3b5..740bb460d 100644 --- a/src/widgets/myspeed/component.test.jsx +++ b/src/widgets/myspeed/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/myspeed/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/netalertx/component.test.jsx b/src/widgets/netalertx/component.test.jsx index a4b077dfc..4ab7b2e68 100644 --- a/src/widgets/netalertx/component.test.jsx +++ b/src/widgets/netalertx/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/netalertx/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/netdata/component.test.jsx b/src/widgets/netdata/component.test.jsx index be0341a16..ec084e7a6 100644 --- a/src/widgets/netdata/component.test.jsx +++ b/src/widgets/netdata/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/netdata/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/npm/component.test.jsx b/src/widgets/npm/component.test.jsx index b8316f584..f3c507989 100644 --- a/src/widgets/npm/component.test.jsx +++ b/src/widgets/npm/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/npm/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/nzbget/component.test.jsx b/src/widgets/nzbget/component.test.jsx index bfb076606..bff5f8611 100644 --- a/src/widgets/nzbget/component.test.jsx +++ b/src/widgets/nzbget/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/nzbget/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/octoprint/component.test.jsx b/src/widgets/octoprint/component.test.jsx index e55582be4..2e875362e 100644 --- a/src/widgets/octoprint/component.test.jsx +++ b/src/widgets/octoprint/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/octoprint/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/omada/proxy.js b/src/widgets/omada/proxy.js index 1151826da..04d173de1 100644 --- a/src/widgets/omada/proxy.js +++ b/src/widgets/omada/proxy.js @@ -95,8 +95,8 @@ export default async function omadaProxyHandler(req, res) { if (loginStatus !== 200 || loginResponseData.errorCode > 0) { return res - .status(status) - .json({ error: { message: "Error logging in to Oamda controller", url: loginUrl, data: loginResponseData } }); + .status(loginStatus) + .json({ error: { message: "Error logging in to Omada controller", url: loginUrl, data: loginResponseData } }); } const { token } = loginResponseData.result; @@ -225,7 +225,13 @@ export default async function omadaProxyHandler(req, res) { if (status !== 200 || siteResponseData.errorCode > 0) { logger.debug(`HTTP ${status} getting stats for site ${widget.site} with message ${siteResponseData.msg}`); - return res.status(500).send(data); + return res.status(status === 200 ? 500 : status).json({ + error: { + message: "Error getting stats", + url: siteStatsUrl, + data: siteResponseData, + }, + }); } const alertUrl = diff --git a/src/widgets/omada/proxy.test.js b/src/widgets/omada/proxy.test.js index 2d20431fe..060ab9a35 100644 --- a/src/widgets/omada/proxy.test.js +++ b/src/widgets/omada/proxy.test.js @@ -151,7 +151,7 @@ describe("widgets/omada/proxy", () => { await omadaProxyHandler(req, res); expect(res.statusCode).toBe(200); - expect(res.body.error.message).toBe("Error logging in to Oamda controller"); + expect(res.body.error.message).toBe("Error logging in to Omada controller"); expect(res.body.error.url).toBe("http://omada/api/v2/login"); expect(res.body.error.data).toEqual({ errorCode: 1, msg: "nope" }); }); @@ -288,7 +288,7 @@ describe("widgets/omada/proxy", () => { expect(res.body.error.message).toBe("Error switching site"); }); - it("returns 500 with the raw payload when overview stats retrieval fails (v5)", async () => { + it("returns a structured error when overview stats retrieval fails (v5)", async () => { getServiceWidget.mockResolvedValue({ url: "http://omada", username: "u", password: "p", site: "Default" }); httpProxy @@ -316,6 +316,12 @@ describe("widgets/omada/proxy", () => { await omadaProxyHandler(req, res); expect(res.statusCode).toBe(500); - expect(res.body).toBe(JSON.stringify({ errorCode: 1, msg: "bad" })); + expect(res.body).toEqual({ + error: { + message: "Error getting stats", + url: "http://omada/cid/api/v2/sites/siteid/dashboard/overviewDiagram?token=t¤tPage=1¤tPageSize=1000", + data: { errorCode: 1, msg: "bad" }, + }, + }); }); }); diff --git a/src/widgets/ombi/component.test.jsx b/src/widgets/ombi/component.test.jsx index 972aa596e..522b0e23a 100644 --- a/src/widgets/ombi/component.test.jsx +++ b/src/widgets/ombi/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/ombi/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/opendtu/component.test.jsx b/src/widgets/opendtu/component.test.jsx index d2652986b..388b3cbbb 100644 --- a/src/widgets/opendtu/component.test.jsx +++ b/src/widgets/opendtu/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/opendtu/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/opnsense/component.test.jsx b/src/widgets/opnsense/component.test.jsx index 2fa972ce0..884525f73 100644 --- a/src/widgets/opnsense/component.test.jsx +++ b/src/widgets/opnsense/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/opnsense/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/overseerr/component.test.jsx b/src/widgets/overseerr/component.test.jsx index 689f10aba..b052fb50e 100644 --- a/src/widgets/overseerr/component.test.jsx +++ b/src/widgets/overseerr/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/overseerr/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/pangolin/component.test.jsx b/src/widgets/pangolin/component.test.jsx index 791a25808..4796b2ee1 100644 --- a/src/widgets/pangolin/component.test.jsx +++ b/src/widgets/pangolin/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue, findServiceBlockByLabel } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/pangolin/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/paperlessngx/component.test.jsx b/src/widgets/paperlessngx/component.test.jsx index 8df49f97e..354af3b61 100644 --- a/src/widgets/paperlessngx/component.test.jsx +++ b/src/widgets/paperlessngx/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue, findServiceBlockByLabel } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/paperlessngx/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/pfsense/component.test.jsx b/src/widgets/pfsense/component.test.jsx index dbfa08944..dd3ba2c31 100644 --- a/src/widgets/pfsense/component.test.jsx +++ b/src/widgets/pfsense/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue, findServiceBlockByLabel } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/pfsense/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/photoprism/component.test.jsx b/src/widgets/photoprism/component.test.jsx index 3f95f1f88..317562ebc 100644 --- a/src/widgets/photoprism/component.test.jsx +++ b/src/widgets/photoprism/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/photoprism/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/plantit/component.test.jsx b/src/widgets/plantit/component.test.jsx index 61502f8cf..c81262542 100644 --- a/src/widgets/plantit/component.test.jsx +++ b/src/widgets/plantit/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/plantit/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/plex/component.test.jsx b/src/widgets/plex/component.test.jsx index 301d0b08b..286b5cb60 100644 --- a/src/widgets/plex/component.test.jsx +++ b/src/widgets/plex/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/plex/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/portainer/component.test.jsx b/src/widgets/portainer/component.test.jsx index 49bb5b782..b87845cde 100644 --- a/src/widgets/portainer/component.test.jsx +++ b/src/widgets/portainer/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/portainer/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/prometheus/component.test.jsx b/src/widgets/prometheus/component.test.jsx index f714f653c..eb86d2af0 100644 --- a/src/widgets/prometheus/component.test.jsx +++ b/src/widgets/prometheus/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/prometheus/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/prometheusmetric/component.test.jsx b/src/widgets/prometheusmetric/component.test.jsx index 8df915874..36731050f 100644 --- a/src/widgets/prometheusmetric/component.test.jsx +++ b/src/widgets/prometheusmetric/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/prometheusmetric/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/prowlarr/component.test.jsx b/src/widgets/prowlarr/component.test.jsx index b9111a5db..19dff8cdc 100644 --- a/src/widgets/prowlarr/component.test.jsx +++ b/src/widgets/prowlarr/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/prowlarr/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/proxmox/component.test.jsx b/src/widgets/proxmox/component.test.jsx index 477c34f01..ca61cb171 100644 --- a/src/widgets/proxmox/component.test.jsx +++ b/src/widgets/proxmox/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/proxmox/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/proxmoxvm/component.test.jsx b/src/widgets/proxmoxvm/component.test.jsx index f535d1d3b..fffaacf42 100644 --- a/src/widgets/proxmoxvm/component.test.jsx +++ b/src/widgets/proxmoxvm/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useSWR } = vi.hoisted(() => ({ useSWR: vi.fn() })); vi.mock("swr", () => ({ default: useSWR })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/proxmoxvm/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/pterodactyl/component.test.jsx b/src/widgets/pterodactyl/component.test.jsx index 66762860e..2c89a7b4a 100644 --- a/src/widgets/pterodactyl/component.test.jsx +++ b/src/widgets/pterodactyl/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/pterodactyl/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/pyload/component.test.jsx b/src/widgets/pyload/component.test.jsx index a34dcb45d..c4b347b4d 100644 --- a/src/widgets/pyload/component.test.jsx +++ b/src/widgets/pyload/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/pyload/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/qbittorrent/component.jsx b/src/widgets/qbittorrent/component.jsx index c8f9f6ead..73dfacb5c 100644 --- a/src/widgets/qbittorrent/component.jsx +++ b/src/widgets/qbittorrent/component.jsx @@ -10,13 +10,28 @@ export default function Component({ service }) { const { t } = useTranslation(); const { widget } = service; - const { data: torrentData, error: torrentError } = useWidgetAPI(widget, "torrents"); + const { data: transferData, error: transferError } = useWidgetAPI(widget, "transfer"); + const { data: totalCountData, error: totalCountError } = useWidgetAPI(widget, "torrentCount"); + const { data: completedCountData, error: completedCountError } = useWidgetAPI(widget, "torrentCount", { + filter: "completed", + }); + const { data: leechTorrentData, error: leechTorrentError } = useWidgetAPI( + widget, + widget?.enableLeechProgress ? "torrents" : "", + widget?.enableLeechProgress ? { filter: "downloading" } : undefined, + ); - if (torrentError) { - return ; + const apiError = transferError || totalCountError || completedCountError || leechTorrentError; + if (apiError) { + return ; } - if (!torrentData) { + if ( + !transferData || + totalCountData === undefined || + completedCountData === undefined || + (widget?.enableLeechProgress && !leechTorrentData) + ) { return ( @@ -27,24 +42,15 @@ export default function Component({ service }) { ); } - let rateDl = 0; - let rateUl = 0; - let completed = 0; - const leechTorrents = []; + const rateDl = Number(transferData?.dl_info_speed ?? 0); + const rateUl = Number(transferData?.up_info_speed ?? 0); + const totalCount = Number(totalCountData?.all ?? totalCountData?.count ?? totalCountData ?? 0); + const completedCount = Number( + completedCountData?.completed ?? completedCountData?.count ?? completedCountData?.all ?? completedCountData ?? 0, + ); + const leech = Math.max(0, totalCount - completedCount); - for (let i = 0; i < torrentData.length; i += 1) { - const torrent = torrentData[i]; - rateDl += torrent.dlspeed; - rateUl += torrent.upspeed; - if (torrent.progress === 1) { - completed += 1; - } - if (torrent.state.includes("DL") || torrent.state === "downloading") { - leechTorrents.push(torrent); - } - } - - const leech = torrentData.length - completed; + const leechTorrents = Array.isArray(leechTorrentData) ? [...leechTorrentData] : []; const statePriority = [ "downloading", "forcedDL", @@ -55,7 +61,6 @@ export default function Component({ service }) { "queuedDL", "pausedDL", ]; - leechTorrents.sort((firstTorrent, secondTorrent) => { const firstStateIndex = statePriority.indexOf(firstTorrent.state); const secondStateIndex = statePriority.indexOf(secondTorrent.state); @@ -70,7 +75,7 @@ export default function Component({ service }) { - + {widget?.enableLeechProgress && diff --git a/src/widgets/qbittorrent/component.test.jsx b/src/widgets/qbittorrent/component.test.jsx index d2f46095f..df8ae51c8 100644 --- a/src/widgets/qbittorrent/component.test.jsx +++ b/src/widgets/qbittorrent/component.test.jsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); @@ -15,12 +15,6 @@ vi.mock("../../components/widgets/queue/queueEntry", () => ({ import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/qbittorrent/component", () => { beforeEach(() => { vi.clearAllMocks(); @@ -40,19 +34,38 @@ describe("widgets/qbittorrent/component", () => { expect(screen.getByText("qbittorrent.upload")).toBeInTheDocument(); }); - it("computes leech/seed counts and upload/download rates, and can render leech progress entries", () => { - useWidgetAPI.mockReturnValue({ - data: [ - { name: "A", dlspeed: 10, upspeed: 1, progress: 1, state: "uploading" }, - { name: "B", dlspeed: 5, upspeed: 2, progress: 0.5, state: "downloading", eta: 60, size: 100, amount_left: 50 }, - ], - error: undefined, + it("uses lightweight endpoints for counts/rates and filtered torrents for leech progress", () => { + useWidgetAPI.mockImplementation((_widget, endpoint, query) => { + if (endpoint === "transfer") { + return { data: { dl_info_speed: 15, up_info_speed: 3 }, error: undefined }; + } + if (endpoint === "torrentCount" && !query) { + return { data: 2, error: undefined }; + } + if (endpoint === "torrentCount" && query?.filter === "completed") { + return { data: 1, error: undefined }; + } + if (endpoint === "torrents" && query?.filter === "downloading") { + return { + data: [ + { + name: "B", + progress: 0.5, + state: "downloading", + eta: 60, + size: 100, + amount_left: 50, + }, + ], + error: undefined, + }; + } + return { data: undefined, error: undefined }; }); const service = { widget: { type: "qbittorrent", enableLeechProgress: true } }; const { container } = renderWithProviders(, { settings: { hideErrors: false } }); - // total=2, completed=1 => leech=1 expectBlockValue(container, "qbittorrent.leech", 1); expectBlockValue(container, "qbittorrent.seed", 1); expectBlockValue(container, "qbittorrent.download", 15); diff --git a/src/widgets/qbittorrent/widget.js b/src/widgets/qbittorrent/widget.js index 182ac9d1b..9ec167faf 100644 --- a/src/widgets/qbittorrent/widget.js +++ b/src/widgets/qbittorrent/widget.js @@ -4,8 +4,16 @@ const widget = { proxyHandler: qbittorrentProxyHandler, mappings: { + transfer: { + endpoint: "transfer/info", + }, + torrentCount: { + endpoint: "torrents/count", + optionalParams: ["filter"], + }, torrents: { endpoint: "torrents/info", + optionalParams: ["filter"], }, }, }; diff --git a/src/widgets/qnap/component.test.jsx b/src/widgets/qnap/component.test.jsx index c08450258..c6778660d 100644 --- a/src/widgets/qnap/component.test.jsx +++ b/src/widgets/qnap/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/qnap/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/radarr/component.test.jsx b/src/widgets/radarr/component.test.jsx index 7637f0d3c..09a5c34b2 100644 --- a/src/widgets/radarr/component.test.jsx +++ b/src/widgets/radarr/component.test.jsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); @@ -15,12 +15,6 @@ vi.mock("../../components/widgets/queue/queueEntry", () => ({ import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/radarr/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/readarr/component.test.jsx b/src/widgets/readarr/component.test.jsx index ac66fdee8..7f0c55fb4 100644 --- a/src/widgets/readarr/component.test.jsx +++ b/src/widgets/readarr/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/readarr/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/romm/component.test.jsx b/src/widgets/romm/component.test.jsx index cabe6dec9..28384f8a1 100644 --- a/src/widgets/romm/component.test.jsx +++ b/src/widgets/romm/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/romm/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/sabnzbd/component.test.jsx b/src/widgets/sabnzbd/component.test.jsx index a81db1399..12a278efb 100644 --- a/src/widgets/sabnzbd/component.test.jsx +++ b/src/widgets/sabnzbd/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/sabnzbd/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/scrutiny/component.test.jsx b/src/widgets/scrutiny/component.test.jsx index 68b0d85bd..03e85c4b9 100644 --- a/src/widgets/scrutiny/component.test.jsx +++ b/src/widgets/scrutiny/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/scrutiny/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/seerr/component.jsx b/src/widgets/seerr/component.jsx new file mode 100644 index 000000000..382d81213 --- /dev/null +++ b/src/widgets/seerr/component.jsx @@ -0,0 +1,49 @@ +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; + +import useWidgetAPI from "utils/proxy/use-widget-api"; + +export const seerrDefaultFields = ["pending", "approved", "completed"]; +const MAX_ALLOWED_FIELDS = 4; + +export default function Component({ service }) { + 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"); + const { data: issueData, error: issueError } = useWidgetAPI(widget, isIssueEnabled ? "issue/count" : ""); + if (statsError || (isIssueEnabled && issueError)) { + return ; + } + + if (!statsData || (isIssueEnabled && !issueData)) { + return ( + + + + + + + + + ); + } + + if (statsData.completed === undefined) { + // Newer versions added "completed", fallback to available + widget.fields = widget.fields.filter((field) => field !== "completed"); + widget.fields.push("available"); + } + + return ( + + + + + + + + + ); +} diff --git a/src/widgets/seerr/component.test.jsx b/src/widgets/seerr/component.test.jsx new file mode 100644 index 000000000..a1c51f5c0 --- /dev/null +++ b/src/widgets/seerr/component.test.jsx @@ -0,0 +1,130 @@ +// @vitest-environment jsdom + +import { screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { renderWithProviders } from "test-utils/render-with-providers"; + +const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); +vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); + +import Component, { seerrDefaultFields } from "./component"; + +describe("widgets/seerr/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("defaults fields and filters to 3 blocks while loading when issues are not enabled", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // request/count + .mockReturnValueOnce({ data: undefined, error: undefined }); // issue/count disabled (endpoint = "") + + const service = { widget: { type: "seerr", url: "http://x" } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(seerrDefaultFields); + expect(useWidgetAPI.mock.calls[1][1]).toBe(""); + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expect(screen.getByText("seerr.pending")).toBeInTheDocument(); + expect(screen.getByText("seerr.approved")).toBeInTheDocument(); + expect(screen.getByText("seerr.completed")).toBeInTheDocument(); + expect(screen.queryByText("seerr.available")).toBeNull(); + expect(screen.queryByText("seerr.processing")).toBeNull(); + expect(screen.queryByText("seerr.issues")).toBeNull(); + }); + + it("supports jellyseerr as a legacy alias to seerr", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // request/count + .mockReturnValueOnce({ data: undefined, error: undefined }); // issue/count disabled (endpoint = "") + + const service = { widget: { type: "jellyseerr", url: "http://x" } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(seerrDefaultFields); + expect(useWidgetAPI.mock.calls[1][1]).toBe(""); + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expect(screen.getByText("seerr.pending")).toBeInTheDocument(); + expect(screen.getByText("seerr.approved")).toBeInTheDocument(); + expect(screen.getByText("seerr.completed")).toBeInTheDocument(); + }); + + it("supports overseerr as a legacy alias with the same default fields", () => { + useWidgetAPI + .mockReturnValueOnce({ data: undefined, error: undefined }) // request/count + .mockReturnValueOnce({ data: undefined, error: undefined }); // issue/count disabled (endpoint = "") + + const service = { widget: { type: "overseerr", url: "http://x" } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(seerrDefaultFields); + expect(useWidgetAPI.mock.calls[1][1]).toBe(""); + expect(container.querySelectorAll(".service-block")).toHaveLength(3); + expect(screen.getByText("seerr.pending")).toBeInTheDocument(); + expect(screen.getByText("seerr.approved")).toBeInTheDocument(); + expect(screen.getByText("seerr.completed")).toBeInTheDocument(); + }); + + it("keeps processing as a separate optional field", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { pending: 1, processing: 2, approved: 3, available: 4 }, error: undefined }) + .mockReturnValueOnce({ data: undefined, error: undefined }); // issue/count disabled (endpoint = "") + + const service = { + widget: { type: "overseerr", url: "http://x", fields: ["pending", "processing", "approved", "available"] }, + }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(useWidgetAPI.mock.calls[1][1]).toBe(""); + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("seerr.processing")).toBeInTheDocument(); + expect(screen.getByText("2")).toBeInTheDocument(); + expect(screen.queryByText("seerr.completed")).toBeNull(); + }); + + it("renders issues when enabled (and calls the issue/count endpoint)", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { pending: 1, approved: 2, available: 3, completed: 4 }, error: undefined }) + .mockReturnValueOnce({ data: { open: 1, total: 2 }, error: undefined }); + + const service = { + widget: { type: "seerr", url: "http://x", fields: ["pending", "approved", "completed", "issues"] }, + }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(useWidgetAPI.mock.calls[1][1]).toBe("issue/count"); + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("1 / 2")).toBeInTheDocument(); + expect(screen.getByText("4")).toBeInTheDocument(); + }); + + it("falls back from completed to available on older Seerr responses", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { pending: 1, approved: 2, available: 3 }, error: undefined }) + .mockReturnValueOnce({ data: undefined, error: undefined }); + + const service = { + widget: { type: "seerr", url: "http://x", fields: ["pending", "approved", "completed"] }, + }; + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(service.widget.fields).toEqual(["pending", "approved", "available"]); + expect(screen.getByText("3")).toBeInTheDocument(); + expect(screen.queryByText("seerr.completed")).toBeNull(); + }); + + it("renders error UI when issues are enabled and issue/count errors", () => { + useWidgetAPI + .mockReturnValueOnce({ data: { pending: 0, approved: 0, available: 0 }, error: undefined }) + .mockReturnValueOnce({ data: undefined, error: { message: "nope" } }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0); + expect(screen.getByText("nope")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/jellyseerr/widget.js b/src/widgets/seerr/widget.js similarity index 100% rename from src/widgets/jellyseerr/widget.js rename to src/widgets/seerr/widget.js diff --git a/src/widgets/jellyseerr/widget.test.js b/src/widgets/seerr/widget.test.js similarity index 83% rename from src/widgets/jellyseerr/widget.test.js rename to src/widgets/seerr/widget.test.js index 14ffdfca9..cdcda3a8f 100644 --- a/src/widgets/jellyseerr/widget.test.js +++ b/src/widgets/seerr/widget.test.js @@ -4,7 +4,7 @@ import { expectWidgetConfigShape } from "test-utils/widget-config"; import widget from "./widget"; -describe("jellyseerr widget config", () => { +describe("seerr widget config", () => { it("exports a valid widget config", () => { expectWidgetConfigShape(widget); }); diff --git a/src/widgets/slskd/component.test.jsx b/src/widgets/slskd/component.test.jsx index 2888c6917..4228430d1 100644 --- a/src/widgets/slskd/component.test.jsx +++ b/src/widgets/slskd/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/slskd/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/sonarr/component.test.jsx b/src/widgets/sonarr/component.test.jsx index 4972cd31d..71dd84bb3 100644 --- a/src/widgets/sonarr/component.test.jsx +++ b/src/widgets/sonarr/component.test.jsx @@ -4,7 +4,7 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); @@ -15,12 +15,6 @@ vi.mock("../../components/widgets/queue/queueEntry", () => ({ import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/sonarr/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/sparkyfitness/component.jsx b/src/widgets/sparkyfitness/component.jsx new file mode 100644 index 000000000..c9c1db42b --- /dev/null +++ b/src/widgets/sparkyfitness/component.jsx @@ -0,0 +1,35 @@ +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; +import { useTranslation } from "next-i18next"; +import useWidgetAPI from "utils/proxy/use-widget-api"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + const { data, error } = useWidgetAPI(widget, "stats"); + + if (error) { + return ; + } + + if (!data) { + return ( + + + + + + + ); + } + + return ( + + + + + + + ); +} diff --git a/src/widgets/sparkyfitness/component.test.jsx b/src/widgets/sparkyfitness/component.test.jsx new file mode 100644 index 000000000..b53a9a0e2 --- /dev/null +++ b/src/widgets/sparkyfitness/component.test.jsx @@ -0,0 +1,61 @@ +// @vitest-environment jsdom + +import { screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { renderWithProviders } from "test-utils/render-with-providers"; +import { expectBlockValue } from "test-utils/widget-assertions"; + +const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); +vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); + +import Component from "./component"; + +describe("widgets/sparkyfitness/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("calls the stats endpoint and renders placeholders while loading", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + const service = { widget: { type: "sparkyfitness", url: "http://x" } }; + const { container } = renderWithProviders(, { settings: { hideErrors: false } }); + + expect(useWidgetAPI).toHaveBeenCalledWith(service.widget, "stats"); + expect(container.querySelectorAll(".service-block")).toHaveLength(4); + expect(screen.getByText("sparkyfitness.eaten")).toBeInTheDocument(); + expect(screen.getByText("sparkyfitness.burned")).toBeInTheDocument(); + expect(screen.getByText("sparkyfitness.remaining")).toBeInTheDocument(); + expect(screen.getByText("sparkyfitness.steps")).toBeInTheDocument(); + expect(screen.getAllByText("-")).toHaveLength(4); + }); + + it("renders error UI when widget API errors", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "nope" } }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getAllByText(/widget\.api_error/i).length).toBeGreaterThan(0); + expect(screen.getByText("nope")).toBeInTheDocument(); + }); + + it("renders numeric values when loaded", () => { + useWidgetAPI.mockReturnValue({ + data: { eaten: 100, burned: 200, remaining: 300, steps: 400 }, + error: undefined, + }); + + const { container } = renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expectBlockValue(container, "sparkyfitness.eaten", 100); + expectBlockValue(container, "sparkyfitness.burned", 200); + expectBlockValue(container, "sparkyfitness.remaining", 300); + expectBlockValue(container, "sparkyfitness.steps", 400); + }); +}); diff --git a/src/widgets/sparkyfitness/widget.js b/src/widgets/sparkyfitness/widget.js new file mode 100644 index 000000000..4447fc943 --- /dev/null +++ b/src/widgets/sparkyfitness/widget.js @@ -0,0 +1,15 @@ +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; + +const widget = { + api: "{url}/{endpoint}", + proxyHandler: credentialedProxyHandler, + + mappings: { + stats: { + endpoint: "api/dashboard/stats", + validate: ["eaten", "burned", "remaining", "steps"], + }, + }, +}; + +export default widget; diff --git a/src/widgets/sparkyfitness/widget.test.js b/src/widgets/sparkyfitness/widget.test.js new file mode 100644 index 000000000..50889a3a1 --- /dev/null +++ b/src/widgets/sparkyfitness/widget.test.js @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; + +import { expectWidgetConfigShape } from "test-utils/widget-config"; + +import widget from "./widget"; + +describe("sparkyfitness widget config", () => { + it("exports a valid widget config", () => { + expectWidgetConfigShape(widget); + + const statsMapping = widget.mappings?.stats; + expect(statsMapping?.endpoint).toBe("api/dashboard/stats"); + expect(statsMapping?.validate).toEqual(["eaten", "burned", "remaining", "steps"]); + }); +}); diff --git a/src/widgets/speedtest/component.test.jsx b/src/widgets/speedtest/component.test.jsx index 87e9e83b1..54b89558e 100644 --- a/src/widgets/speedtest/component.test.jsx +++ b/src/widgets/speedtest/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/speedtest/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/spoolman/component.test.jsx b/src/widgets/spoolman/component.test.jsx index 1d467fe20..be5ea1d87 100644 --- a/src/widgets/spoolman/component.test.jsx +++ b/src/widgets/spoolman/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/spoolman/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/stash/component.test.jsx b/src/widgets/stash/component.test.jsx index 798ec6e17..201bb9e29 100644 --- a/src/widgets/stash/component.test.jsx +++ b/src/widgets/stash/component.test.jsx @@ -4,16 +4,10 @@ import { screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/stash/component", () => { const originalFetch = globalThis.fetch; diff --git a/src/widgets/strelaysrv/component.test.jsx b/src/widgets/strelaysrv/component.test.jsx index 578a17062..933cd628d 100644 --- a/src/widgets/strelaysrv/component.test.jsx +++ b/src/widgets/strelaysrv/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/strelaysrv/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/suwayomi/component.test.jsx b/src/widgets/suwayomi/component.test.jsx index 3945be219..a3a779d85 100644 --- a/src/widgets/suwayomi/component.test.jsx +++ b/src/widgets/suwayomi/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/suwayomi/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/tailscale/component.test.jsx b/src/widgets/tailscale/component.test.jsx index d3214b06a..602d6ba1c 100644 --- a/src/widgets/tailscale/component.test.jsx +++ b/src/widgets/tailscale/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue, findServiceBlockByLabel } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/tailscale/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/tandoor/component.test.jsx b/src/widgets/tandoor/component.test.jsx index e3ab09efc..40162469e 100644 --- a/src/widgets/tandoor/component.test.jsx +++ b/src/widgets/tandoor/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/tandoor/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/tdarr/component.test.jsx b/src/widgets/tdarr/component.test.jsx index 051a5d366..918b5c6c6 100644 --- a/src/widgets/tdarr/component.test.jsx +++ b/src/widgets/tdarr/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/tdarr/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/technitium/component.test.jsx b/src/widgets/technitium/component.test.jsx index af54b493a..7888d687d 100644 --- a/src/widgets/technitium/component.test.jsx +++ b/src/widgets/technitium/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue, findServiceBlockByLabel } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component, { technitiumDefaultFields } from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/technitium/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/tracearr/component.jsx b/src/widgets/tracearr/component.jsx new file mode 100644 index 000000000..41a52b66c --- /dev/null +++ b/src/widgets/tracearr/component.jsx @@ -0,0 +1,268 @@ +/* eslint-disable camelcase */ +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; +import { useTranslation } from "next-i18next"; +import { BsCpu, BsFillCpuFill, BsFillPlayFill, BsPauseFill } from "react-icons/bs"; +import { MdOutlineSmartDisplay, MdSmartDisplay } from "react-icons/md"; + +import useWidgetAPI from "utils/proxy/use-widget-api"; + +function millisecondsToTime(milliseconds) { + const seconds = Math.floor((milliseconds / 1000) % 60); + const minutes = Math.floor((milliseconds / (1000 * 60)) % 60); + const hours = Math.floor((milliseconds / (1000 * 60 * 60)) % 24); + return { hours, minutes, seconds }; +} + +function millisecondsToString(milliseconds) { + const { hours, minutes, seconds } = millisecondsToTime(milliseconds); + const parts = []; + if (hours > 0) { + parts.push(hours); + } + parts.push(minutes); + parts.push(seconds); + + return parts.map((part) => part.toString().padStart(2, "0")).join(":"); +} + +function generateStreamTitle(session, enableUser, showEpisodeNumber) { + let stream_title = ""; + const { mediaType, mediaTitle, showTitle, seasonNumber, episodeNumber, username } = session; + + if (mediaType === "episode" && showEpisodeNumber) { + const season_str = `S${seasonNumber.toString().padStart(2, "0")}`; + const episode_str = `E${episodeNumber.toString().padStart(2, "0")}`; + stream_title = `${showTitle}: ${season_str} · ${episode_str} - ${mediaTitle}`; + } else if (mediaType === "episode") { + stream_title = `${showTitle} - ${mediaTitle}`; + } else { + stream_title = mediaTitle; + } + + return enableUser ? `${stream_title} (${username})` : stream_title; +} + +function SingleSessionEntry({ session, enableUser, showEpisodeNumber }) { + const { durationMs, progressMs, state, videoDecision, audioDecision } = session; + const progress_percent = durationMs > 0 ? (progressMs / durationMs) * 100 : 0; + + const stream_title = generateStreamTitle(session, enableUser, showEpisodeNumber); + + return ( + <> +
+
+
+ {stream_title} +
+
+
+ {videoDecision === "directplay" && audioDecision === "directplay" && ( + + )} + {videoDecision === "copy" && audioDecision === "copy" && } + {videoDecision !== "copy" && + videoDecision !== "directplay" && + (audioDecision !== "copy" || audioDecision !== "directplay") && } + {(videoDecision === "copy" || videoDecision === "directplay") && + audioDecision !== "copy" && + audioDecision !== "directplay" && } +
+
+ +
+
+
+ {state === "paused" && ( + + )} + {state !== "paused" && ( + + )} +
+
+
+ {millisecondsToString(progressMs)} + / + {millisecondsToString(durationMs)} +
+
+ + ); +} + +function SessionEntry({ session, enableUser, showEpisodeNumber }) { + const { durationMs, progressMs, state, videoDecision, audioDecision } = session; + const progress_percent = durationMs > 0 ? (progressMs / durationMs) * 100 : 0; + + const stream_title = generateStreamTitle(session, enableUser, showEpisodeNumber); + + return ( +
+
+
+ {state === "paused" && ( + + )} + {state !== "paused" && ( + + )} +
+
+
+ {stream_title} +
+
+
+ {videoDecision === "directplay" && audioDecision === "directplay" && } + {videoDecision === "copy" && audioDecision === "copy" && } + {videoDecision !== "copy" && + videoDecision !== "directplay" && + (audioDecision !== "copy" || audioDecision !== "directplay") && } + {(videoDecision === "copy" || videoDecision === "directplay") && + audioDecision !== "copy" && + audioDecision !== "directplay" && } +
+
{millisecondsToString(progressMs)}
+
+ ); +} + +function SummaryView({ service, summary, t }) { + return ( + + + + + + + ); +} + +function DetailsView({ playing, enableUser, showEpisodeNumber, expandOneStreamToTwoRows, t }) { + if (playing.length === 0) { + return ( +
+
+ {t("tracearr.no_active")} +
+ {expandOneStreamToTwoRows && ( +
+ - +
+ )} +
+ ); + } + + if (expandOneStreamToTwoRows && playing.length === 1) { + const session = playing[0]; + return ( +
+ +
+ ); + } + + return ( +
+ {playing.map((session) => ( + + ))} +
+ ); +} + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + + const { data: activityData, error: activityError } = useWidgetAPI(widget, "streams", { + refreshInterval: 5000, + }); + + const enableUser = !!service.widget?.enableUser; + const expandOneStreamToTwoRows = service.widget?.expandOneStreamToTwoRows !== false; + const showEpisodeNumber = !!service.widget?.showEpisodeNumber; + const view = service.widget?.view ?? "details"; + + if (activityError) { + return ; + } + + // Loading state + if (!activityData || !activityData.data) { + if (view === "summary") { + return ( + + + + + + + ); + } + return ( +
+
+ - +
+ {expandOneStreamToTwoRows && ( +
+ - +
+ )} +
+ ); + } + + const playing = activityData.data.sort((a, b) => a.progressMs - b.progressMs); + const { summary } = activityData; + + if (view === "summary") { + return ; + } + + if (view === "both") { + return ( + <> + + + + ); + } + + // Default: details view + return ( + + ); +} diff --git a/src/widgets/tracearr/component.test.jsx b/src/widgets/tracearr/component.test.jsx new file mode 100644 index 000000000..c9b54e950 --- /dev/null +++ b/src/widgets/tracearr/component.test.jsx @@ -0,0 +1,391 @@ +// @vitest-environment jsdom + +import { screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { renderWithProviders } from "test-utils/render-with-providers"; + +vi.mock("react-icons/bs", () => ({ + BsCpu: (props) => , + BsFillCpuFill: (props) => , + BsFillPlayFill: (props) => , + BsPauseFill: (props) => , +})); + +vi.mock("react-icons/md", () => ({ + MdOutlineSmartDisplay: (props) => , + MdSmartDisplay: (props) => , +})); + +const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); +vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); + +import Component from "./component"; + +describe("widgets/tracearr/component", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders placeholder rows while loading", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: undefined }); + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(screen.getAllByText("-").length).toBeGreaterThan(0); + }); + + it("renders placeholder blocks while loading in summary view", () => { + useWidgetAPI.mockReturnValue({ data: { data: null }, error: undefined }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("tracearr.streams")).toBeInTheDocument(); + expect(screen.getByText("tracearr.transcodes")).toBeInTheDocument(); + expect(screen.getByText("tracearr.directplay")).toBeInTheDocument(); + expect(screen.getByText("tracearr.bitrate")).toBeInTheDocument(); + }); + + it("renders errors from the widget API", () => { + useWidgetAPI.mockReturnValue({ data: undefined, error: { message: "boom" } }); + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(screen.getByText(/widget\.api_error\s+widget\.information/)).toBeInTheDocument(); + expect(screen.getByText(/boom/)).toBeInTheDocument(); + }); + + it("renders no-active message when there are no streams", () => { + useWidgetAPI.mockReturnValue({ + data: { data: [], summary: { total: 0, transcodes: 0, directPlays: 0, totalBitrate: "0 Mbps" } }, + error: undefined, + }); + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(screen.getByText("tracearr.no_active")).toBeInTheDocument(); + }); + + it("renders an expanded two-row entry when a single stream is playing", () => { + useWidgetAPI.mockReturnValue({ + data: { + data: [ + { + id: "1", + mediaTitle: "Inception", + mediaType: "movie", + durationMs: 7200000, // 2 hours + progressMs: 2700000, // 45 minutes in + state: "playing", + videoDecision: "directplay", + audioDecision: "directplay", + }, + ], + summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" }, + }, + error: undefined, + }); + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(screen.getByText("Inception")).toBeInTheDocument(); + expect(screen.getByText(/45:00/)).toBeInTheDocument(); // 45 minutes in + expect(screen.getByText(/02:00:00/)).toBeInTheDocument(); // 2 hour duration + }); + + it("uses 0% progress when duration is 0 in expanded view", () => { + useWidgetAPI.mockReturnValue({ + data: { + data: [ + { + id: "1", + mediaTitle: "Short Clip", + mediaType: "movie", + durationMs: 0, + progressMs: 5000, + state: "playing", + videoDecision: "directplay", + audioDecision: "directplay", + }, + ], + summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "1 Mbps" }, + }, + error: undefined, + }); + + const { container } = renderWithProviders(, { + settings: { hideErrors: false }, + }); + + const bars = container.querySelectorAll('div[style*="width"]'); + expect(bars.length).toBeGreaterThan(0); + expect(bars[0]).toHaveStyle({ width: "0%" }); + }); + + it("renders episode title with season/episode and username when configured", () => { + useWidgetAPI.mockReturnValue({ + data: { + data: [ + { + id: "2", + mediaTitle: "Ozymandias", + showTitle: "Breaking Bad", + mediaType: "episode", + seasonNumber: 5, + episodeNumber: 14, + durationMs: 2700000, + progressMs: 1200000, + state: "playing", + videoDecision: "directplay", + audioDecision: "directplay", + username: "Walter", + }, + ], + summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "10 Mbps" }, + }, + error: undefined, + }); + + renderWithProviders( + , + { settings: { hideErrors: false } }, + ); + + expect(screen.getByText("Breaking Bad: S05 · E14 - Ozymandias (Walter)")).toBeInTheDocument(); + }); + + it("renders multiple streams including movie and tv episode", () => { + useWidgetAPI.mockReturnValue({ + data: { + data: [ + { + id: "1", + mediaTitle: "Inception", + mediaType: "movie", + durationMs: 7200000, // 2 hours + progressMs: 2700000, // 45 minutes in + state: "playing", + videoDecision: "directplay", + audioDecision: "directplay", + }, + { + id: "2", + mediaTitle: "Ozymandias", + showTitle: "Breaking Bad", + mediaType: "episode", + seasonNumber: 5, + episodeNumber: 14, + durationMs: 2700000, // 45 minutes + progressMs: 1200000, // 20 minutes in + state: "playing", + videoDecision: "transcode", + audioDecision: "directplay", + username: "Walter", + }, + ], + summary: { total: 2, transcodes: 1, directPlays: 1, totalBitrate: "35 Mbps" }, + }, + error: undefined, + }); + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(screen.getByText("Inception")).toBeInTheDocument(); + expect(screen.getByText("Breaking Bad - Ozymandias")).toBeInTheDocument(); + }); + + it.each([ + ["copy/copy shows copy icon", { videoDecision: "copy", audioDecision: "copy" }, "MdOutlineSmartDisplay"], + ["transcode shows cpu fill icon", { videoDecision: "transcode", audioDecision: "directplay" }, "BsFillCpuFill"], + ["transcode+copy shows cpu fill icon", { videoDecision: "transcode", audioDecision: "copy" }, "BsFillCpuFill"], + ["mixed transcode shows cpu icon", { videoDecision: "directplay", audioDecision: "transcode" }, "BsCpu"], + ])("renders transcoding indicators in expanded view: %s", (_label, decisions, expectedIcon) => { + useWidgetAPI.mockReturnValue({ + data: { + data: [ + { + id: "1", + mediaTitle: "Inception", + mediaType: "movie", + durationMs: 7200000, + progressMs: 2700000, + state: "playing", + ...decisions, + }, + ], + summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" }, + }, + error: undefined, + }); + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(screen.getByTestId(expectedIcon)).toBeInTheDocument(); + }); + + it("renders a pause icon when a stream is paused in expanded view", () => { + useWidgetAPI.mockReturnValue({ + data: { + data: [ + { + id: "1", + mediaTitle: "Inception", + mediaType: "movie", + durationMs: 7200000, + progressMs: 2700000, + state: "paused", + videoDecision: "directplay", + audioDecision: "directplay", + }, + ], + summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" }, + }, + error: undefined, + }); + + renderWithProviders(, { settings: { hideErrors: false } }); + + expect(screen.getByTestId("BsPauseFill")).toBeInTheDocument(); + }); + + it.each([ + ["copy/copy shows copy icon", { videoDecision: "copy", audioDecision: "copy" }, "MdOutlineSmartDisplay"], + ["transcode shows cpu fill icon", { videoDecision: "transcode", audioDecision: "directplay" }, "BsFillCpuFill"], + ["transcode+copy shows cpu fill icon", { videoDecision: "transcode", audioDecision: "copy" }, "BsFillCpuFill"], + ["mixed transcode shows cpu icon", { videoDecision: "directplay", audioDecision: "transcode" }, "BsCpu"], + ])("renders transcoding indicators in single-row view: %s", (_label, decisions, expectedIcon) => { + useWidgetAPI.mockReturnValue({ + data: { + data: [ + { + id: "1", + mediaTitle: "Inception", + mediaType: "movie", + durationMs: 7200000, + progressMs: 2700000, + state: "playing", + ...decisions, + }, + ], + summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" }, + }, + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByTestId(expectedIcon)).toBeInTheDocument(); + }); + + it("renders a pause icon when a stream is paused in single-row view", () => { + useWidgetAPI.mockReturnValue({ + data: { + data: [ + { + id: "1", + mediaTitle: "Inception", + mediaType: "movie", + durationMs: 7200000, + progressMs: 2700000, + state: "paused", + videoDecision: "directplay", + audioDecision: "directplay", + }, + ], + summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" }, + }, + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByTestId("BsPauseFill")).toBeInTheDocument(); + }); + + it("uses 0% progress when duration is 0 in single-row view", () => { + useWidgetAPI.mockReturnValue({ + data: { + data: [ + { + id: "1", + mediaTitle: "Short Clip", + mediaType: "movie", + durationMs: 0, + progressMs: 5000, + state: "playing", + videoDecision: "directplay", + audioDecision: "directplay", + }, + ], + summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "1 Mbps" }, + }, + error: undefined, + }); + + const { container } = renderWithProviders( + , + { + settings: { hideErrors: false }, + }, + ); + + const bars = container.querySelectorAll('div[style*="width"]'); + expect(bars.length).toBeGreaterThan(0); + expect(bars[0]).toHaveStyle({ width: "0%" }); + }); + + it("renders summary view when view option is set to summary", () => { + useWidgetAPI.mockReturnValue({ + data: { + data: [], + summary: { total: 5, transcodes: 2, directPlays: 3, totalBitrate: "45 Mbps" }, + }, + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("tracearr.streams")).toBeInTheDocument(); + expect(screen.getByText("tracearr.bitrate")).toBeInTheDocument(); + }); + + it("renders both summary and details when view option is set to both", () => { + useWidgetAPI.mockReturnValue({ + data: { + data: [ + { + id: "1", + mediaTitle: "Inception", + mediaType: "movie", + durationMs: 7200000, + progressMs: 2700000, + state: "playing", + videoDecision: "directplay", + audioDecision: "directplay", + }, + ], + summary: { total: 1, transcodes: 0, directPlays: 1, totalBitrate: "20 Mbps" }, + }, + error: undefined, + }); + + renderWithProviders(, { + settings: { hideErrors: false }, + }); + + expect(screen.getByText("tracearr.streams")).toBeInTheDocument(); + expect(screen.getByText("Inception")).toBeInTheDocument(); + }); +}); diff --git a/src/widgets/tracearr/widget.js b/src/widgets/tracearr/widget.js new file mode 100644 index 000000000..5e52a1513 --- /dev/null +++ b/src/widgets/tracearr/widget.js @@ -0,0 +1,14 @@ +import credentialedProxyHandler from "utils/proxy/handlers/credentialed"; + +const widget = { + api: "{url}/api/v1/public/{endpoint}", + proxyHandler: credentialedProxyHandler, + + mappings: { + streams: { + endpoint: "streams", + }, + }, +}; + +export default widget; diff --git a/src/widgets/tracearr/widget.test.js b/src/widgets/tracearr/widget.test.js new file mode 100644 index 000000000..3980a41a4 --- /dev/null +++ b/src/widgets/tracearr/widget.test.js @@ -0,0 +1,11 @@ +import { describe, it } from "vitest"; + +import { expectWidgetConfigShape } from "test-utils/widget-config"; + +import widget from "./widget"; + +describe("tracearr widget config", () => { + it("exports a valid widget config", () => { + expectWidgetConfigShape(widget); + }); +}); diff --git a/src/widgets/traefik/component.test.jsx b/src/widgets/traefik/component.test.jsx index 9f1c9313a..fb638612f 100644 --- a/src/widgets/traefik/component.test.jsx +++ b/src/widgets/traefik/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/traefik/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/transmission/component.test.jsx b/src/widgets/transmission/component.test.jsx index 95d2593e4..25b702add 100644 --- a/src/widgets/transmission/component.test.jsx +++ b/src/widgets/transmission/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/transmission/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/trilium/component.test.jsx b/src/widgets/trilium/component.test.jsx index 6d930b45d..ca7e17111 100644 --- a/src/widgets/trilium/component.test.jsx +++ b/src/widgets/trilium/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/trilium/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/tubearchivist/component.test.jsx b/src/widgets/tubearchivist/component.test.jsx index 5f181d233..b56b765ef 100644 --- a/src/widgets/tubearchivist/component.test.jsx +++ b/src/widgets/tubearchivist/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/tubearchivist/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/unifi/component.test.jsx b/src/widgets/unifi/component.test.jsx index 2947a25ef..ff2e2da00 100644 --- a/src/widgets/unifi/component.test.jsx +++ b/src/widgets/unifi/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue, findServiceBlockByLabel } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/unifi/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/unmanic/component.test.jsx b/src/widgets/unmanic/component.test.jsx index 94b2acea8..cdaae3970 100644 --- a/src/widgets/unmanic/component.test.jsx +++ b/src/widgets/unmanic/component.test.jsx @@ -4,19 +4,13 @@ import { screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/unmanic/component", () => { const originalFetch = globalThis.fetch; diff --git a/src/widgets/uptimekuma/component.test.jsx b/src/widgets/uptimekuma/component.test.jsx index 7052d4a91..03065f782 100644 --- a/src/widgets/uptimekuma/component.test.jsx +++ b/src/widgets/uptimekuma/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/uptimekuma/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/uptimerobot/component.test.jsx b/src/widgets/uptimerobot/component.test.jsx index a5f7f3395..cce4626c0 100644 --- a/src/widgets/uptimerobot/component.test.jsx +++ b/src/widgets/uptimerobot/component.test.jsx @@ -4,16 +4,10 @@ import { screen, waitFor } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/uptimerobot/component", () => { const originalFetch = globalThis.fetch; diff --git a/src/widgets/urbackup/component.test.jsx b/src/widgets/urbackup/component.test.jsx index 06223dba3..125f0576e 100644 --- a/src/widgets/urbackup/component.test.jsx +++ b/src/widgets/urbackup/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/urbackup/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/vikunja/component.test.jsx b/src/widgets/vikunja/component.test.jsx index 494d4f49e..6968c853d 100644 --- a/src/widgets/vikunja/component.test.jsx +++ b/src/widgets/vikunja/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/vikunja/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/wallos/component.test.jsx b/src/widgets/wallos/component.test.jsx index f03fd03fa..7006ed3f2 100644 --- a/src/widgets/wallos/component.test.jsx +++ b/src/widgets/wallos/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/wallos/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/watchtower/component.test.jsx b/src/widgets/watchtower/component.test.jsx index 03cf94bdd..857c40b2b 100644 --- a/src/widgets/watchtower/component.test.jsx +++ b/src/widgets/watchtower/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/watchtower/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/wgeasy/component.test.jsx b/src/widgets/wgeasy/component.test.jsx index 40c1d6180..eebc83915 100644 --- a/src/widgets/wgeasy/component.test.jsx +++ b/src/widgets/wgeasy/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/wgeasy/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/whatsupdocker/component.test.jsx b/src/widgets/whatsupdocker/component.test.jsx index df96f8cb9..7bdcf3c45 100644 --- a/src/widgets/whatsupdocker/component.test.jsx +++ b/src/widgets/whatsupdocker/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/whatsupdocker/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 5142ee23c..533410bdc 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -56,7 +56,6 @@ import immich from "./immich/widget"; import jackett from "./jackett/widget"; import jdownloader from "./jdownloader/widget"; import jellyfin from "./jellyfin/widget"; -import jellyseerr from "./jellyseerr/widget"; import jellystat from "./jellystat/widget"; import karakeep from "./karakeep/widget"; import kavita from "./kavita/widget"; @@ -91,7 +90,6 @@ import opendtu from "./opendtu/widget"; import openmediavault from "./openmediavault/widget"; import openwrt from "./openwrt/widget"; import opnsense from "./opnsense/widget"; -import overseerr from "./overseerr/widget"; import pangolin from "./pangolin/widget"; import paperlessngx from "./paperlessngx/widget"; import peanut from "./peanut/widget"; @@ -116,8 +114,10 @@ import romm from "./romm/widget"; import rutorrent from "./rutorrent/widget"; import sabnzbd from "./sabnzbd/widget"; import scrutiny from "./scrutiny/widget"; +import seerr from "./seerr/widget"; import slskd from "./slskd/widget"; import sonarr from "./sonarr/widget"; +import sparkyfitness from "./sparkyfitness/widget"; import speedtest from "./speedtest/widget"; import spoolman from "./spoolman/widget"; import stash from "./stash/widget"; @@ -130,6 +130,7 @@ import tandoor from "./tandoor/widget"; import tautulli from "./tautulli/widget"; import tdarr from "./tdarr/widget"; import technitium from "./technitium/widget"; +import tracearr from "./tracearr/widget"; import traefik from "./traefik/widget"; import transmission from "./transmission/widget"; import trilium from "./trilium/widget"; @@ -211,7 +212,7 @@ const widgets = { jackett, jdownloader, jellyfin, - jellyseerr, + jellyseerr: seerr, jellystat, kavita, komga, @@ -243,7 +244,7 @@ const widgets = { ombi, opendtu, opnsense, - overseerr, + overseerr: seerr, openmediavault, openwrt, paperlessngx, @@ -271,8 +272,10 @@ const widgets = { rutorrent, sabnzbd, scrutiny, + seerr, slskd, sonarr, + sparkyfitness, speedtest, spoolman, stash, @@ -285,6 +288,7 @@ const widgets = { tautulli, technitium, tdarr, + tracearr, traefik, transmission, trilium, diff --git a/src/widgets/xteve/component.test.jsx b/src/widgets/xteve/component.test.jsx index caeb1b2e5..ee639f554 100644 --- a/src/widgets/xteve/component.test.jsx +++ b/src/widgets/xteve/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/xteve/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/yourspotify/component.test.jsx b/src/widgets/yourspotify/component.test.jsx index 6e60b46b7..589fcf689 100644 --- a/src/widgets/yourspotify/component.test.jsx +++ b/src/widgets/yourspotify/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/yourspotify/component", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/src/widgets/zabbix/component.test.jsx b/src/widgets/zabbix/component.test.jsx index 0795eb0bc..25047022a 100644 --- a/src/widgets/zabbix/component.test.jsx +++ b/src/widgets/zabbix/component.test.jsx @@ -4,19 +4,13 @@ import { screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "test-utils/render-with-providers"; -import { findServiceBlockByLabel } from "test-utils/widget-assertions"; +import { expectBlockValue } from "test-utils/widget-assertions"; const { useWidgetAPI } = vi.hoisted(() => ({ useWidgetAPI: vi.fn() })); vi.mock("utils/proxy/use-widget-api", () => ({ default: useWidgetAPI })); import Component from "./component"; -function expectBlockValue(container, label, value) { - const block = findServiceBlockByLabel(container, label); - expect(block, `missing block for ${label}`).toBeTruthy(); - expect(block.textContent).toContain(String(value)); -} - describe("widgets/zabbix/component", () => { beforeEach(() => { vi.clearAllMocks();